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
450 lines
14 KiB
Python
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
|