mirror of
https://github.com/kuhyx/testsAndMisc-archive.git
synced 2026-11-10 23:07:05 +01:00
billsplit was a standalone Flutter app in ~/Downloads with one commit and no git remote — that directory was the only copy of its history. Three things the move needed beyond copying the tree: - The root .gitignore silently swallowed 32 of its files (*.png, *.ico, *.manifest). Ignored files do not error on `git add`, they just vanish, so this would have committed an app that cannot build. Added a trailing block of ! overrides plus .binary-allowlist (which did not exist yet) — the documented in-repo exception for binaries a build genuinely needs. The ../testsAndMisc_binaries/ symlink scheme does not apply: those directories are gone, and a symlinked PNG breaks a fresh clone. - billsplit/.github/workflows/ci.yaml was inert once nested; GitHub only reads workflows at the repository root. Relocated with a working-directory and a billsplit/** path filter. - The app's coverage gate moved to python_pkg/billsplit_coverage/ so every Python file stays under python_pkg/, and gained a test suite at 100% branch coverage as that location requires. billsplit/ now holds only Dart. Also replaces the stock Flutter icon with a family icon from the shared generator. The glyph is a filled silhouette because the family's 72px stroke cannot carry a fine torn edge, and the split is an evenodd-cut perforation so the holes stay transparent on the foreground and monochrome layers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
192 lines
7.7 KiB
Python
192 lines
7.7 KiB
Python
"""Tests for the billsplit coverage gate."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import runpy
|
|
import sys
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from python_pkg.billsplit_coverage import checker
|
|
|
|
if TYPE_CHECKING:
|
|
import pathlib
|
|
|
|
|
|
def make_project(
|
|
root: pathlib.Path,
|
|
lcov: str | None = None,
|
|
dart_files: tuple[str, ...] = ("lib/main.dart",),
|
|
) -> pathlib.Path:
|
|
"""Build a throwaway Flutter project tree.
|
|
|
|
Parameters:
|
|
root (pathlib.Path): Directory to build the project in.
|
|
lcov (str | None): Contents of ``coverage/lcov.info``; None writes no
|
|
report at all, which is the missing-report case.
|
|
dart_files (tuple[str, ...]): Project-relative Dart files to create.
|
|
|
|
Returns:
|
|
pathlib.Path: The project root.
|
|
"""
|
|
for relative in dart_files:
|
|
path = root / relative
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text("void main() {}\n", encoding="utf-8")
|
|
if lcov is not None:
|
|
report = root / "coverage" / "lcov.info"
|
|
report.parent.mkdir(parents=True, exist_ok=True)
|
|
report.write_text(lcov, encoding="utf-8")
|
|
return root
|
|
|
|
|
|
class TestParseLcov:
|
|
def test_records_only_unhit_lines(self, tmp_path: pathlib.Path) -> None:
|
|
report = tmp_path / "lcov.info"
|
|
report.write_text(
|
|
"SF:lib/main.dart\nDA:1,3\nDA:2,0\nDA:9,0\nend_of_record\n",
|
|
encoding="utf-8",
|
|
)
|
|
assert checker.parse_lcov(report) == {"lib/main.dart": [2, 9]}
|
|
|
|
def test_fully_covered_file_maps_to_empty_list(
|
|
self, tmp_path: pathlib.Path
|
|
) -> None:
|
|
"""Present-and-covered must be distinguishable from absent."""
|
|
report = tmp_path / "lcov.info"
|
|
report.write_text("SF:lib/a.dart\nDA:1,1\nend_of_record\n", encoding="utf-8")
|
|
assert checker.parse_lcov(report) == {"lib/a.dart": []}
|
|
|
|
def test_normalises_windows_separators(self, tmp_path: pathlib.Path) -> None:
|
|
report = tmp_path / "lcov.info"
|
|
report.write_text("SF:lib\\ui\\home.dart\nDA:1,1\n", encoding="utf-8")
|
|
assert "lib/ui/home.dart" in checker.parse_lcov(report)
|
|
|
|
def test_da_before_any_sf_is_ignored(self, tmp_path: pathlib.Path) -> None:
|
|
"""A DA: line with no preceding SF: has no file to attribute to."""
|
|
report = tmp_path / "lcov.info"
|
|
report.write_text("DA:1,0\nSF:lib/a.dart\nDA:2,0\n", encoding="utf-8")
|
|
assert checker.parse_lcov(report) == {"lib/a.dart": [2]}
|
|
|
|
def test_unrelated_lines_are_skipped(self, tmp_path: pathlib.Path) -> None:
|
|
report = tmp_path / "lcov.info"
|
|
report.write_text(
|
|
"TN:\nSF:lib/a.dart\nLF:2\nLH:1\nDA:1,0\nend_of_record\n",
|
|
encoding="utf-8",
|
|
)
|
|
assert checker.parse_lcov(report) == {"lib/a.dart": [1]}
|
|
|
|
def test_empty_report_yields_nothing(self, tmp_path: pathlib.Path) -> None:
|
|
report = tmp_path / "lcov.info"
|
|
report.write_text("", encoding="utf-8")
|
|
assert checker.parse_lcov(report) == {}
|
|
|
|
|
|
class TestFindFailures:
|
|
def test_no_gaps_returns_empty(self, tmp_path: pathlib.Path) -> None:
|
|
project = make_project(tmp_path, lcov="")
|
|
assert checker.find_failures({"lib/main.dart": []}, project) == []
|
|
|
|
def test_reports_uncovered_lines(self, tmp_path: pathlib.Path) -> None:
|
|
project = make_project(tmp_path, lcov="")
|
|
failures = checker.find_failures({"lib/main.dart": [2, 7]}, project)
|
|
assert failures == ["lib/main.dart: uncovered lines [2, 7]"]
|
|
|
|
def test_reports_file_absent_from_report(self, tmp_path: pathlib.Path) -> None:
|
|
project = make_project(
|
|
tmp_path, lcov="", dart_files=("lib/main.dart", "lib/orphan.dart")
|
|
)
|
|
failures = checker.find_failures({"lib/main.dart": []}, project)
|
|
assert failures == ["lib/orphan.dart: not executed by any test (0% coverage)"]
|
|
|
|
def test_reports_both_kinds_together(self, tmp_path: pathlib.Path) -> None:
|
|
project = make_project(
|
|
tmp_path, lcov="", dart_files=("lib/main.dart", "lib/orphan.dart")
|
|
)
|
|
failures = checker.find_failures({"lib/main.dart": [4]}, project)
|
|
assert failures == [
|
|
"lib/main.dart: uncovered lines [4]",
|
|
"lib/orphan.dart: not executed by any test (0% coverage)",
|
|
]
|
|
|
|
def test_finds_dart_files_in_subdirectories(self, tmp_path: pathlib.Path) -> None:
|
|
project = make_project(
|
|
tmp_path, lcov="", dart_files=("lib/domain/split_engine.dart",)
|
|
)
|
|
failures = checker.find_failures({}, project)
|
|
assert failures == [
|
|
"lib/domain/split_engine.dart: not executed by any test (0% coverage)"
|
|
]
|
|
|
|
|
|
class TestCheck:
|
|
def test_missing_report_exits_two(
|
|
self, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
project = make_project(tmp_path)
|
|
assert checker.check(project) == checker.EXIT_NO_REPORT
|
|
assert "flutter test --coverage" in capsys.readouterr().err
|
|
|
|
def test_incomplete_coverage_exits_one_and_names_files(
|
|
self, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
project = make_project(
|
|
tmp_path,
|
|
lcov="SF:lib/main.dart\nDA:1,1\nDA:2,0\n",
|
|
dart_files=("lib/main.dart", "lib/orphan.dart"),
|
|
)
|
|
assert checker.check(project) == checker.EXIT_INCOMPLETE
|
|
err = capsys.readouterr().err
|
|
assert "COVERAGE < 100%:" in err
|
|
assert "lib/main.dart: uncovered lines [2]" in err
|
|
assert "lib/orphan.dart: not executed by any test" in err
|
|
|
|
def test_full_coverage_exits_zero(
|
|
self, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
project = make_project(tmp_path, lcov="SF:lib/main.dart\nDA:1,1\n")
|
|
assert checker.check(project) == 0
|
|
assert "100% line coverage across 1 lib files." in capsys.readouterr().out
|
|
|
|
|
|
class TestMain:
|
|
def test_project_flag_selects_the_tree(self, tmp_path: pathlib.Path) -> None:
|
|
project = make_project(tmp_path, lcov="SF:lib/main.dart\nDA:1,1\n")
|
|
assert checker.main(["--project", str(project)]) == 0
|
|
|
|
def test_defaults_to_the_repo_billsplit_app(self) -> None:
|
|
"""With no --project, the gate resolves billsplit/ from this file."""
|
|
with patch.object(checker, "check", return_value=0) as mock_check:
|
|
assert checker.main([]) == 0
|
|
assert mock_check.call_args[0][0].name == "billsplit"
|
|
|
|
def test_propagates_failure_status(self, tmp_path: pathlib.Path) -> None:
|
|
project = make_project(tmp_path)
|
|
assert checker.main(["--project", str(project)]) == checker.EXIT_NO_REPORT
|
|
|
|
|
|
class TestModuleEntry:
|
|
def test_main_called_when_run_as_module(self) -> None:
|
|
"""python3 -m python_pkg.billsplit_coverage runs main() and exits."""
|
|
mock_main = MagicMock(return_value=0)
|
|
# runpy warns if __main__ is already imported, and the suite may run in
|
|
# a random order, so another test could have imported it first.
|
|
sys.modules.pop("python_pkg.billsplit_coverage.__main__", None)
|
|
with (
|
|
patch("python_pkg.billsplit_coverage.checker.main", mock_main),
|
|
pytest.raises(SystemExit) as excinfo,
|
|
):
|
|
runpy.run_module("python_pkg.billsplit_coverage", run_name="__main__")
|
|
assert excinfo.value.code == 0
|
|
mock_main.assert_called_once()
|
|
|
|
def test_main_not_called_on_plain_import(self) -> None:
|
|
"""Importing __main__ must not run the gate as a side effect."""
|
|
mock_main = MagicMock()
|
|
with patch("python_pkg.billsplit_coverage.checker.main", mock_main):
|
|
sys.modules.pop("python_pkg.billsplit_coverage.__main__", None)
|
|
runpy.run_module("python_pkg.billsplit_coverage.__main__")
|
|
mock_main.assert_not_called()
|