Files
learningcircuit--local-deep…/tests/hooks/test_commit_analysis.py
T
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

450 lines
14 KiB
Python

"""
Tests for the _commit_analysis pre-commit hook shared module.
Covers: classify_file, suggest_test_path, _parse_numstat,
CommitAnalysis dataclass, and analyze_commit (with mocked git).
"""
import sys
from pathlib import Path
from unittest.mock import patch
# Add the pre-commit hooks directory to path
HOOKS_DIR = Path(__file__).parent.parent.parent / ".pre-commit-hooks"
sys.path.insert(0, str(HOOKS_DIR))
from _commit_analysis import ( # noqa: E402
CommitAnalysis,
StagedFileInfo,
_parse_numstat,
analyze_commit,
classify_file,
suggest_test_path,
)
# =========================================================================
# classify_file tests
# =========================================================================
class TestClassifyFile:
"""Tests for classify_file path classification."""
# --- Source files ---
def test_source_file_under_src(self):
assert classify_file("src/local_deep_research/web/api.py") == "source"
def test_source_file_deep_nesting(self):
assert (
classify_file(
"src/local_deep_research/web_search_engines/engines/search_engine_google.py"
)
== "source"
)
def test_source_file_direct_under_src(self):
assert classify_file("src/local_deep_research/main.py") == "source"
# --- Test files ---
def test_test_file_under_tests_dir(self):
assert classify_file("tests/web/test_api.py") == "test"
def test_test_file_by_prefix(self):
assert classify_file("src/test_something.py") == "test"
def test_test_file_by_suffix(self):
assert classify_file("src/something_test.py") == "test"
def test_test_file_nested_in_tests(self):
assert classify_file("tests/security/test_url_validator.py") == "test"
# --- Doc files ---
def test_markdown_file(self):
assert classify_file("README.md") == "doc"
def test_markdown_file_in_docs(self):
assert classify_file("docs/setup.md") == "doc"
def test_markdown_file_nested(self):
assert classify_file("src/local_deep_research/CHANGELOG.md") == "doc"
# --- Excluded directories ---
def test_migrations_dir(self):
assert (
classify_file("src/local_deep_research/migrations/001.py")
== "migration"
)
def test_alembic_dir(self):
assert classify_file("alembic/versions/abc123.py") == "migration"
def test_config_dir(self):
assert (
classify_file("src/local_deep_research/config/settings.py")
== "config"
)
def test_settings_dir(self):
assert classify_file("settings/defaults.py") == "config"
def test_scripts_dir(self):
assert classify_file("scripts/dev/restart_server.py") == "script"
def test_pre_commit_hooks_dir(self):
assert classify_file(".pre-commit-hooks/check-env-vars.py") == "hook"
def test_examples_dir(self):
assert classify_file("examples/basic_usage.py") == "other"
def test_docs_dir_python_file(self):
assert classify_file("docs/conf.py") == "doc"
# --- Special basenames ---
def test_init_py(self):
assert classify_file("src/local_deep_research/__init__.py") == "init"
def test_conftest_py(self):
assert classify_file("tests/conftest.py") == "conftest"
# --- Non-Python files ---
def test_json_file(self):
assert (
classify_file(
"src/local_deep_research/defaults/default_settings.json"
)
== "other"
)
def test_yaml_file(self):
assert classify_file(".pre-commit-config.yaml") == "other"
def test_html_file(self):
assert (
classify_file("src/local_deep_research/web/templates/index.html")
== "other"
)
def test_javascript_file(self):
assert (
classify_file("src/local_deep_research/web/static/app.js")
== "other"
)
# --- Edge cases ---
def test_top_level_python_file(self):
assert classify_file("setup.py") == "other"
def test_empty_path_components(self):
"""Files not under src/ and not matching other patterns are 'other'."""
assert classify_file("lib/helper.py") == "other"
def test_case_insensitive_excluded_dirs(self):
"""Excluded dirs match case-insensitively."""
assert (
classify_file("src/local_deep_research/Config/app.py") == "config"
)
def test_test_prefix_not_in_tests_dir(self):
"""test_*.py files are classified as test even outside tests/."""
assert classify_file("src/test_helpers.py") == "test"
# =========================================================================
# suggest_test_path tests
# =========================================================================
class TestSuggestTestPath:
"""Tests for suggest_test_path path generation."""
def test_standard_source_file(self):
result = suggest_test_path("src/local_deep_research/web/api.py")
assert result == "tests/web/test_api.py"
def test_deeply_nested_source(self):
result = suggest_test_path(
"src/local_deep_research/web_search_engines/engines/search_engine_google.py"
)
assert (
result
== "tests/web_search_engines/engines/test_search_engine_google.py"
)
def test_direct_under_package(self):
result = suggest_test_path("src/local_deep_research/main.py")
assert result == "tests/test_main.py"
def test_strips_src_prefix(self):
"""Should strip src/ prefix from path."""
result = suggest_test_path("src/local_deep_research/utils.py")
assert result.startswith("tests/")
assert "src" not in result
def test_adds_test_prefix_to_filename(self):
"""Should add test_ prefix to the filename."""
result = suggest_test_path("src/local_deep_research/database/models.py")
assert result.endswith("test_models.py")
# =========================================================================
# _parse_numstat tests
# =========================================================================
class TestParseNumstat:
"""Tests for _parse_numstat git output parsing."""
def test_normal_output(self):
lines = [
"10\t5\tsrc/local_deep_research/web/api.py",
"20\t3\ttests/web/test_api.py",
]
result = _parse_numstat(lines)
assert result == {
"src/local_deep_research/web/api.py": (10, 5),
"tests/web/test_api.py": (20, 3),
}
def test_binary_file(self):
"""Binary files show '-' for added/removed counts."""
lines = ["-\t-\timages/logo.png"]
result = _parse_numstat(lines)
assert result == {"images/logo.png": (0, 0)}
def test_empty_input(self):
assert _parse_numstat([]) == {}
def test_malformed_line_skipped(self):
"""Lines with wrong number of fields are skipped."""
lines = [
"10\t5\tsrc/api.py",
"bad line",
"20\t3\ttests/test_api.py",
]
result = _parse_numstat(lines)
assert len(result) == 2
assert "src/api.py" in result
assert "tests/test_api.py" in result
def test_zero_counts(self):
lines = ["0\t0\tempty_file.py"]
result = _parse_numstat(lines)
assert result == {"empty_file.py": (0, 0)}
def test_large_counts(self):
lines = ["9999\t8888\tbig_file.py"]
result = _parse_numstat(lines)
assert result == {"big_file.py": (9999, 8888)}
def test_filepath_with_spaces(self):
"""Tabs separate fields, so filenames with spaces work."""
lines = ["5\t2\tpath with spaces/file.py"]
result = _parse_numstat(lines)
assert "path with spaces/file.py" in result
# =========================================================================
# CommitAnalysis dataclass tests
# =========================================================================
class TestCommitAnalysis:
"""Tests for CommitAnalysis computed properties."""
def _make_file(
self,
path="src/a.py",
added=10,
removed=0,
is_new=False,
category="source",
):
return StagedFileInfo(
path=path,
added=added,
removed=removed,
is_new=is_new,
category=category,
)
def test_empty_analysis(self):
analysis = CommitAnalysis()
assert analysis.total_source_added == 0
assert analysis.has_tests is False
assert analysis.has_docs is False
assert analysis.new_source_files == []
def test_total_source_added(self):
analysis = CommitAnalysis(
source_files=[
self._make_file(added=10),
self._make_file(added=20),
]
)
assert analysis.total_source_added == 30
def test_has_tests_true(self):
analysis = CommitAnalysis(test_files=[self._make_file(category="test")])
assert analysis.has_tests is True
def test_has_tests_false(self):
analysis = CommitAnalysis(source_files=[self._make_file()])
assert analysis.has_tests is False
def test_has_docs_true(self):
analysis = CommitAnalysis(doc_files=[self._make_file(category="doc")])
assert analysis.has_docs is True
def test_new_source_files(self):
analysis = CommitAnalysis(
source_files=[
self._make_file(path="a.py", is_new=True),
self._make_file(path="b.py", is_new=False),
self._make_file(path="c.py", is_new=True),
]
)
new = analysis.new_source_files
assert len(new) == 2
assert all(f.is_new for f in new)
# =========================================================================
# analyze_commit integration tests (with mocked git)
# =========================================================================
class TestAnalyzeCommit:
"""Tests for analyze_commit with mocked git commands."""
def _mock_run_git(self, name_status_lines, numstat_lines):
"""Create a mock for _run_git returning different output per command."""
def side_effect(args):
if "--name-status" in args:
return name_status_lines
if "--numstat" in args:
return numstat_lines
return []
return side_effect
def test_basic_source_and_test_commit(self):
mock = self._mock_run_git(
name_status_lines=[
"M\tsrc/local_deep_research/web/api.py",
"A\ttests/web/test_api.py",
],
numstat_lines=[
"15\t3\tsrc/local_deep_research/web/api.py",
"50\t0\ttests/web/test_api.py",
],
)
with patch("_commit_analysis._run_git", side_effect=mock):
analysis = analyze_commit()
assert len(analysis.source_files) == 1
assert len(analysis.test_files) == 1
assert (
analysis.source_files[0].path
== "src/local_deep_research/web/api.py"
)
assert analysis.source_files[0].is_new is False
assert analysis.test_files[0].is_new is True
assert analysis.has_tests is True
def test_new_file_detection(self):
mock = self._mock_run_git(
name_status_lines=["A\tsrc/local_deep_research/new_module.py"],
numstat_lines=["100\t0\tsrc/local_deep_research/new_module.py"],
)
with patch("_commit_analysis._run_git", side_effect=mock):
analysis = analyze_commit()
assert len(analysis.source_files) == 1
assert analysis.source_files[0].is_new is True
assert analysis.source_files[0].added == 100
def test_doc_files_classified(self):
mock = self._mock_run_git(
name_status_lines=["A\tREADME.md"],
numstat_lines=["20\t0\tREADME.md"],
)
with patch("_commit_analysis._run_git", side_effect=mock):
analysis = analyze_commit()
assert len(analysis.doc_files) == 1
assert analysis.has_docs is True
def test_excluded_files_not_in_any_list(self):
"""Config, migration, and other excluded files don't appear in analysis lists."""
mock = self._mock_run_git(
name_status_lines=[
"M\tsrc/local_deep_research/config/settings.py",
"M\t.pre-commit-config.yaml",
],
numstat_lines=[
"5\t2\tsrc/local_deep_research/config/settings.py",
"3\t1\t.pre-commit-config.yaml",
],
)
with patch("_commit_analysis._run_git", side_effect=mock):
analysis = analyze_commit()
assert len(analysis.source_files) == 0
assert len(analysis.test_files) == 0
assert len(analysis.doc_files) == 0
def test_empty_commit(self):
mock = self._mock_run_git(
name_status_lines=[],
numstat_lines=[],
)
with patch("_commit_analysis._run_git", side_effect=mock):
analysis = analyze_commit()
assert len(analysis.source_files) == 0
assert analysis.total_source_added == 0
assert analysis.has_tests is False
assert analysis.has_docs is False
def test_mixed_commit_with_all_types(self):
mock = self._mock_run_git(
name_status_lines=[
"A\tsrc/local_deep_research/feature.py",
"A\ttests/test_feature.py",
"M\tREADME.md",
"M\tscripts/dev/helper.py",
],
numstat_lines=[
"80\t0\tsrc/local_deep_research/feature.py",
"40\t0\ttests/test_feature.py",
"10\t5\tREADME.md",
"3\t1\tscripts/dev/helper.py",
],
)
with patch("_commit_analysis._run_git", side_effect=mock):
analysis = analyze_commit()
assert len(analysis.source_files) == 1
assert len(analysis.test_files) == 1
assert len(analysis.doc_files) == 1
assert analysis.total_source_added == 80
assert analysis.has_tests is True
assert analysis.has_docs is True
assert analysis.source_files[0].is_new is True