🚧 Add export feature and refined exclusion info
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
"""Tests for exclusion reporting and log-level policy (Sections 7 and 22).
|
||||
|
||||
Excluded paths and the rule that matched them are logged at DEBUG level;
|
||||
the INFO summary carries only the in-scope/excluded split. Commit messages
|
||||
never contain exclusion details (Section 7).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from git_auto_tracker.changes.filters import (
|
||||
Exclusion,
|
||||
exclusion_reason,
|
||||
filter_change_set,
|
||||
inclusion_reason,
|
||||
)
|
||||
from git_auto_tracker.changes.models import ChangeRecord, ChangeSet, ChangeType
|
||||
from git_auto_tracker.config import FileFilters
|
||||
|
||||
|
||||
class TestReasonFunctions:
|
||||
def test_exclusion_reason_names_the_pattern(self) -> None:
|
||||
filters = FileFilters(include=["**/*"], exclude=["runtime/**", "**/*.log"])
|
||||
assert exclusion_reason("runtime/cache.dat", filters) == "matched exclude 'runtime/**'"
|
||||
assert exclusion_reason("logs/a.log", filters) == "matched exclude '**/*.log'"
|
||||
assert exclusion_reason("keep.xml", filters) is None
|
||||
|
||||
def test_inclusion_reason_when_out_of_scope(self) -> None:
|
||||
filters = FileFilters(include=["Stations/**"], exclude=[])
|
||||
assert inclusion_reason("System/global.xml", filters) == "no include pattern matched"
|
||||
assert inclusion_reason("Stations/PS1.xml", filters) is None
|
||||
|
||||
def test_empty_include_means_everything_included(self) -> None:
|
||||
filters = FileFilters(include=[], exclude=["**/*.tmp"])
|
||||
assert inclusion_reason("anything.xml", filters) is None
|
||||
|
||||
def test_legacy_predicates_still_work(self) -> None:
|
||||
filters = FileFilters(include=["**/*"], exclude=["**/*.log"])
|
||||
from git_auto_tracker.changes.filters import is_excluded, is_included
|
||||
|
||||
assert is_included("a.xml", filters) is True
|
||||
assert is_excluded("a.log", filters) is True
|
||||
assert is_excluded("a.xml", filters) is False
|
||||
|
||||
|
||||
class TestFilterChangeSet:
|
||||
def _change_set(self) -> ChangeSet:
|
||||
return ChangeSet(
|
||||
added=[
|
||||
ChangeRecord(path="runtime/cache.dat", change_type=ChangeType.ADDED),
|
||||
ChangeRecord(path="Stations/PS1.xml", change_type=ChangeType.ADDED),
|
||||
],
|
||||
modified=[
|
||||
ChangeRecord(path="logs/app.log", change_type=ChangeType.MODIFIED),
|
||||
ChangeRecord(path="System/global.xml", change_type=ChangeType.MODIFIED),
|
||||
],
|
||||
)
|
||||
|
||||
def test_exclusions_carry_reasons(self) -> None:
|
||||
filters = FileFilters(include=["**/*"], exclude=["runtime/**", "**/*.log"])
|
||||
kept, excluded = filter_change_set(self._change_set(), filters)
|
||||
|
||||
assert [r.path for r in kept.added] == ["Stations/PS1.xml"]
|
||||
assert [r.path for r in kept.modified] == ["System/global.xml"]
|
||||
by_path = {e.path: e.reason for e in excluded}
|
||||
assert by_path["runtime/cache.dat"] == "matched exclude 'runtime/**'"
|
||||
assert by_path["logs/app.log"] == "matched exclude '**/*.log'"
|
||||
|
||||
def test_out_of_scope_excluded_with_include_reason(self) -> None:
|
||||
filters = FileFilters(include=["Stations/**"], exclude=["**/*.log"])
|
||||
_, excluded = filter_change_set(self._change_set(), filters)
|
||||
by_path = {e.path: e.reason for e in excluded}
|
||||
assert by_path["System/global.xml"] == "no include pattern matched"
|
||||
assert by_path["runtime/cache.dat"] == "no include pattern matched"
|
||||
assert by_path["logs/app.log"] == "matched exclude '**/*.log'"
|
||||
|
||||
def test_exclusion_is_dataclass(self) -> None:
|
||||
record = Exclusion(path="x.tmp", reason="matched exclude '**/*.tmp'")
|
||||
assert record.path == "x.tmp"
|
||||
assert "x.tmp" in record.reason or "**/*.tmp" in record.reason
|
||||
|
||||
|
||||
class TestLogLevels:
|
||||
"""INFO summary only; per-file detail at DEBUG (application wiring)."""
|
||||
|
||||
def test_collect_logs_split_and_debug_details(
|
||||
self, git_repo, tracker_config, tmp_path, caplog
|
||||
) -> None:
|
||||
from git_auto_tracker.application import ApplicationController
|
||||
from git_auto_tracker.config import AppConfig
|
||||
from git_auto_tracker.logging.setup import setup_logging
|
||||
|
||||
(git_repo / "junk.log").write_text("noise", encoding="utf-8")
|
||||
(git_repo / "real.xml").write_text("<r/>", encoding="utf-8")
|
||||
|
||||
config = AppConfig.from_dict(tracker_config, config_dir=tmp_path)
|
||||
setup_logging(
|
||||
config.logging,
|
||||
config_dir=tmp_path,
|
||||
repository_path=config.repository_path,
|
||||
override_level="DEBUG",
|
||||
)
|
||||
# The app logger does not propagate (setup_logging), so caplog's
|
||||
# root-attached handler never sees the records; attach it directly.
|
||||
app_logger = logging.getLogger("git_auto_tracker")
|
||||
app_logger.addHandler(caplog.handler)
|
||||
try:
|
||||
controller = ApplicationController(config)
|
||||
outcome = controller.collect()
|
||||
finally:
|
||||
app_logger.removeHandler(caplog.handler)
|
||||
|
||||
assert outcome.change_count == 1 # real.xml only
|
||||
debug_text = caplog.text
|
||||
|
||||
# INFO summary: the split, no per-file exclusion detail
|
||||
assert "Detected 2 change(s): 1 in scope, 1 excluded by filters" in debug_text
|
||||
# DEBUG detail: path plus the matching rule
|
||||
assert "Excluded: junk.log (matched exclude '**/*.log')" in debug_text
|
||||
# Author resolution details land at DEBUG too
|
||||
assert "Author resolution detail: real.xml -> source=" in debug_text
|
||||
# The exclusion must NOT appear in the commit message (Section 7)
|
||||
assert "junk.log" not in (outcome.commit_message or "")
|
||||
Reference in New Issue
Block a user