blob: db519ca9a28841a51c96b84f0bff9b8bf494cbf7 [file]
#!/usr/bin/env python3
#
# Copyright 2026 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for .github/scripts/check_file_sizes.py and .githooks/pre-commit."""
import contextlib
from contextlib import redirect_stderr
import io
import os
import shutil
import stat
import subprocess
import tempfile
import unittest
from unittest import mock
import check_file_sizes
class CheckFileSizesTest(unittest.TestCase):
"""Tests for .github/scripts/check_file_sizes.py and .githooks/pre-commit."""
@contextlib.contextmanager
def _temp_repo_dir(self):
repo_dir = tempfile.mkdtemp()
try:
yield repo_dir
finally:
def _handle_readonly(func, path, _exc_info):
os.chmod(path, stat.S_IWRITE)
func(path)
shutil.rmtree(repo_dir, onerror=_handle_readonly)
def _git(self, repo_dir, *args):
subprocess.run(
[
"git",
"-C",
repo_dir,
"-c",
"user.name=Test User",
"-c",
"user.email=test@example.com",
"-c",
"commit.gpgsign=false",
*args,
],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
def _write_file(self, repo_dir, rel_path, size_bytes, fill_byte=b"x"):
full_path = os.path.join(repo_dir, rel_path)
os.makedirs(os.path.dirname(full_path), exist_ok=True)
with open(full_path, "wb") as f:
f.write(fill_byte * size_bytes)
def test_default_limit_and_repo_dir(self):
self.assertEqual(check_file_sizes.BYTES_PER_MIB, 1024 * 1024)
self.assertEqual(check_file_sizes.MAX_FILE_SIZE_BYTES, 25 * 1024 * 1024)
self.assertTrue(
os.path.isfile(
os.path.join(check_file_sizes.DEFAULT_REPO_DIR, ".githooks", "pre-commit")
)
)
def test_allows_blobs_at_or_below_limit_and_rejects_above_limit(self):
one_mib = 1024 * 1024
with self._temp_repo_dir() as repo_dir:
self._git(repo_dir, "init")
self._git(repo_dir, "checkout", "-B", "master")
self._write_file(repo_dir, "exact_limit.bin", one_mib, fill_byte=b"\0")
self._git(repo_dir, "add", "exact_limit.bin")
self._git(repo_dir, "commit", "-m", "Add file at limit")
self.assertEqual(
check_file_sizes.find_oversized_blobs(
repo_dir, base_ref="origin/master", max_bytes=one_mib
),
[],
)
self._write_file(
repo_dir, "dir with spaces/over limit.bin", one_mib + 1, fill_byte=b"\0"
)
self._git(repo_dir, "add", "dir with spaces/over limit.bin")
self._git(repo_dir, "commit", "-m", "Add oversized file")
oversized = check_file_sizes.find_oversized_blobs(
repo_dir, base_ref="origin/master", max_bytes=one_mib
)
self.assertEqual(oversized, [("dir with spaces/over limit.bin", one_mib + 1)])
exit_code, stderr_text = self._run_cli(
repo_dir, max_bytes=one_mib, base_ref="origin/master"
)
self.assertEqual(exit_code, 1)
self.assertIn("exceed the 1 MiB (1,048,576 bytes) limit", stderr_text)
self.assertIn(
"dir with spaces/over limit.bin: 1.00 MiB (1,048,577 bytes)", stderr_text
)
self.assertIn("internal clone of this repository", stderr_text)
def test_catches_oversized_blob_added_then_deleted_in_commit_range(self):
with self._temp_repo_dir() as repo_dir:
self._git(repo_dir, "init")
self._git(repo_dir, "checkout", "-B", "master")
self._write_file(repo_dir, "small.txt", 32)
self._git(repo_dir, "add", "small.txt")
self._git(repo_dir, "commit", "-m", "Initial commit")
self._git(repo_dir, "update-ref", "refs/remotes/origin/master", "HEAD")
self._git(repo_dir, "checkout", "-b", "feature")
self._write_file(repo_dir, "transient_archive.tar", 2048)
self._git(repo_dir, "add", "transient_archive.tar")
self._git(repo_dir, "commit", "-m", "Add large archive")
self._git(repo_dir, "rm", "transient_archive.tar")
self._git(repo_dir, "commit", "-m", "Delete large archive")
oversized = check_file_sizes.find_oversized_blobs(
repo_dir, base_ref="origin/master", max_bytes=1000
)
self.assertEqual(oversized, [("transient_archive.tar", 2048)])
def test_fetches_github_base_ref_when_missing_locally(self):
with self._temp_repo_dir() as origin_dir, self._temp_repo_dir() as clone_dir:
self._git(origin_dir, "init", "--bare")
self._git(clone_dir, "init")
self._git(clone_dir, "checkout", "-B", "master")
self._git(clone_dir, "remote", "add", "origin", origin_dir)
self._write_file(clone_dir, "small.txt", 32)
self._git(clone_dir, "add", "small.txt")
self._git(clone_dir, "commit", "-m", "Initial commit")
self._git(clone_dir, "push", "origin", "master")
self._git(clone_dir, "update-ref", "-d", "refs/remotes/origin/master")
self._git(clone_dir, "checkout", "-b", "pr-branch")
self._write_file(clone_dir, "temp_bundle.bin", 2048)
self._git(clone_dir, "add", "temp_bundle.bin")
self._git(clone_dir, "commit", "-m", "Add oversized file")
self._git(clone_dir, "rm", "temp_bundle.bin")
self._git(clone_dir, "commit", "-m", "Remove oversized file")
with mock.patch.dict(os.environ, {"GITHUB_BASE_REF": "master"}):
exit_code, stderr_text = self._run_cli(clone_dir, max_bytes=1000)
self.assertEqual(exit_code, 1)
self.assertIn("temp_bundle.bin", stderr_text)
@unittest.skipIf(os.name == "nt", "Bash pre-commit hook test runs on POSIX")
def test_pre_commit_hook_enforces_staged_blob_size_limit(self):
hook_path = os.path.join(
check_file_sizes.DEFAULT_REPO_DIR, ".githooks", "pre-commit"
)
default_limit = 25 * 1024 * 1024
with self._temp_repo_dir() as repo_dir:
self._git(repo_dir, "init")
self._git(repo_dir, "checkout", "-B", "master")
self._write_file(repo_dir, "exact_25mib.bin", default_limit, fill_byte=b"\0")
self._git(repo_dir, "add", "exact_25mib.bin")
env = os.environ.copy()
env.pop("MAX_FILE_SIZE_BYTES", None)
ok_proc = subprocess.run(
["bash", hook_path],
cwd=repo_dir,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
self.assertEqual(ok_proc.returncode, 0, msg=ok_proc.stderr)
self._write_file(
repo_dir, "sub dir/too_big.bin", default_limit + 1, fill_byte=b"\0"
)
self._git(repo_dir, "add", "sub dir/too_big.bin")
bad_proc = subprocess.run(
["bash", hook_path],
cwd=repo_dir,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
self.assertEqual(bad_proc.returncode, 1)
self.assertIn(
f"sub dir/too_big.bin ({default_limit + 1} bytes > {default_limit} bytes)",
bad_proc.stderr,
)
self.assertIn("internal clone of this repository", bad_proc.stderr)
def _run_cli(self, repo_dir, max_bytes, base_ref=None):
cli_args = ["--repo-dir", repo_dir, "--max-bytes", str(max_bytes)]
if base_ref is not None:
cli_args.extend(["--base-ref", base_ref])
err = io.StringIO()
with redirect_stderr(err):
code = check_file_sizes.main(cli_args)
return code, err.getvalue()
if __name__ == "__main__":
unittest.main()