mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-24 18:23:01 +08:00
269 lines
15 KiB
Python
269 lines
15 KiB
Python
"""Regression tests for missed changes and false verification scope."""
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
try:
|
|
from affected.repository import Repository, test_classes
|
|
from affected.planner import Planner
|
|
except ImportError:
|
|
Repository = Planner = test_classes = None
|
|
|
|
|
|
class RepositoryCase(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory(prefix="atomui-selection-test-")
|
|
self.addCleanup(self.temp.cleanup)
|
|
self.root = Path(self.temp.name)
|
|
self.git("init", "-q")
|
|
self.git("config", "user.email", "test@example.invalid")
|
|
self.git("config", "user.name", "Test")
|
|
self.write(".gitignore", ".artifacts/\n__pycache__/\n")
|
|
self.write("src/Controls/Controls.csproj", '<Project Sdk="Microsoft.NET.Sdk"/>')
|
|
self.write("src/Controls/Button/Button.cs", "namespace Product; public class Button {}")
|
|
self.write("src/Controls/Popup/Popup.cs", "namespace Product; public class Popup {}")
|
|
self.write("src/Controls/Select/Select.cs", "namespace Product; public class Select { Popup popup; }")
|
|
self.write("tests/UI.Tests/UI.Tests.csproj", '<Project><ItemGroup><PackageReference Include="xunit.v3"/><ProjectReference Include="../../src/Controls/Controls.csproj"/></ItemGroup></Project>')
|
|
for name, uses in [("Button", "Button"), ("Popup", "Popup"), ("Select", "Select")]:
|
|
self.write("tests/UI.Tests/" + name + "/" + name + "Tests.cs", "namespace Tests; public class " + name + "Tests { [Fact] public void Works() { new " + uses + "(); } }")
|
|
self.write("tests/Integration.Tests/Integration.Tests.csproj", '<Project><ItemGroup><PackageReference Include="xunit.v3"/></ItemGroup></Project>')
|
|
self.write("tests/Integration.Tests/Checks.cs", "namespace Integration; public class Checks { [Theory] [InlineData(1)] public void Works(int a) {} }")
|
|
self.policy = {
|
|
"version": 1,
|
|
"areas": [{"id": "controls", "roots": ["src/Controls"], "depth": 1, "tests": ["tests/UI.Tests/**/*.cs"]}],
|
|
"rules": [{"id": "integration", "changes": ["inputs/**"], "tests": ["tests/Integration.Tests/**/*.cs"]}],
|
|
"ignore": ["docs/**", ".gitignore"],
|
|
"tooling": ["scripts/verification/**"],
|
|
"obligations": [],
|
|
}
|
|
self.write("scripts/verification/test-policy.json", json.dumps(self.policy))
|
|
self.git("add", ".")
|
|
self.git("commit", "-qm", "baseline")
|
|
|
|
def git(self, *args):
|
|
return subprocess.check_output(["git", "-C", str(self.root), *args], text=True).strip()
|
|
|
|
def write(self, path, text):
|
|
destination = self.root / path
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
destination.write_text(text)
|
|
|
|
def planner(self):
|
|
self.assertIsNotNone(Planner, "Affected verification planner has not been implemented")
|
|
return Planner(Repository(self.root), self.policy)
|
|
|
|
def selected(self, plan):
|
|
return {name for job in plan["tests"] for name in job["classes"]}
|
|
|
|
|
|
class PlannerTests(RepositoryCase):
|
|
def test_control_change_selects_its_cross_project_tests_without_unrelated_controls(self):
|
|
plan = self.planner().plan(["src/Controls/Button/Button.cs"])
|
|
self.assertEqual({"Tests.ButtonTests"}, self.selected(plan))
|
|
self.assertFalse(plan["gaps"])
|
|
|
|
def test_unchanged_sibling_type_does_not_expand_consumers(self):
|
|
self.write("src/Controls/Button/OtherControl.cs", "class OtherControl {}")
|
|
self.write("src/Controls/Select/Select.cs", "class Select { OtherControl item; }")
|
|
plan = self.planner().plan(["src/Controls/Button/Button.cs"])
|
|
self.assertNotIn("Tests.SelectTests", self.selected(plan))
|
|
|
|
def test_consumer_directory_does_not_pull_in_unrelated_sibling_types(self):
|
|
self.write("src/Controls/Primitives/ButtonPresenter.cs", "class ButtonPresenter { Button item; }")
|
|
self.write("src/Controls/Primitives/UnrelatedBase.cs", "class UnrelatedBase {}")
|
|
self.write("tests/UI.Tests/Primitives/UnrelatedTests.cs", "namespace Tests; class UnrelatedTests { [Fact] void Works() { new UnrelatedBase(); } }")
|
|
self.write("tests/UI.Tests/Primitives/PresenterTests.cs", "namespace Tests; class PresenterTests { [Fact] void Works() { new ButtonPresenter(); } }")
|
|
plan = self.planner().plan(["src/Controls/Button/Button.cs"])
|
|
self.assertIn("Tests.PresenterTests", self.selected(plan))
|
|
self.assertNotIn("Tests.UnrelatedTests", self.selected(plan))
|
|
|
|
def test_shared_popup_change_includes_transitive_consumer_tests(self):
|
|
plan = self.planner().plan(["src/Controls/Popup/Popup.cs"])
|
|
self.assertEqual({"Tests.PopupTests", "Tests.SelectTests"}, self.selected(plan))
|
|
|
|
def test_theme_target_change_includes_control_consumers(self):
|
|
self.write("src/Controls/Popup/Themes/PopupTheme.axaml", '<ControlTheme TargetType="atom:Popup" x:Class="Themes.PopupTheme"/>')
|
|
plan = self.planner().plan(["src/Controls/Popup/Themes/PopupTheme.axaml"])
|
|
self.assertIn("Tests.SelectTests", self.selected(plan))
|
|
|
|
def test_in_family_inheritance_still_reaches_external_consumers(self):
|
|
self.write("src/Controls/Popup/DerivedPopup.cs", "class DerivedPopup : Popup {}")
|
|
self.write("src/Controls/Select/Select.cs", "class Select { DerivedPopup popup; }")
|
|
plan = self.planner().plan(["src/Controls/Popup/Popup.cs"])
|
|
self.assertIn("Tests.SelectTests", self.selected(plan))
|
|
|
|
def test_in_family_composition_selects_its_tests_outside_family_directory(self):
|
|
self.write("src/Controls/Popup/PopupPresenter.cs", "class PopupPresenter { Popup popup; }")
|
|
self.write("tests/UI.Tests/PresentationTests.cs", "namespace Tests; class PresentationTests { [Fact] void Works() { new PopupPresenter(); } }")
|
|
plan = self.planner().plan(["src/Controls/Popup/Popup.cs"])
|
|
self.assertIn("Tests.PresentationTests", self.selected(plan))
|
|
|
|
def test_composed_control_inheritance_is_selected_without_second_composition_hop(self):
|
|
self.write("src/Controls/Button/ButtonPolicy.cs", "class ButtonPolicy {}")
|
|
self.write("src/Controls/Button/Button.cs", "class Button { ButtonPolicy policy; }")
|
|
self.write("src/Controls/Dropdown/Dropdown.cs", "class Dropdown : Button {}")
|
|
self.write("src/Controls/Toolbar/Toolbar.cs", "class Toolbar { Dropdown control; }")
|
|
for name in ["Dropdown", "Toolbar"]:
|
|
self.write("tests/UI.Tests/" + name + "Tests.cs", "namespace Tests; class " + name + "Tests { [Fact] void Works() { new " + name + "(); } }")
|
|
plan = self.planner().plan(["src/Controls/Button/ButtonPolicy.cs"])
|
|
self.assertIn("Tests.DropdownTests", self.selected(plan))
|
|
self.assertNotIn("Tests.ToolbarTests", self.selected(plan))
|
|
|
|
def test_code_in_a_string_is_not_a_runtime_consumer_edge(self):
|
|
self.write("src/Controls/Select/Select.cs", 'class Select { string example = "new Popup()"; }')
|
|
plan = self.planner().plan(["src/Controls/Popup/Popup.cs"])
|
|
self.assertNotIn("Tests.SelectTests", self.selected(plan))
|
|
|
|
def test_unreferenced_project_cannot_create_a_reverse_dependency(self):
|
|
self.write("tools/Generator/Generator.csproj", "<Project/>")
|
|
self.write("tools/Generator/Generator.cs", "class Generator { Popup symbol; }")
|
|
self.policy["areas"].append({"id": "generator", "roots": ["tools/Generator"], "depth": 0, "tests": ["tests/Integration.Tests/**/*.cs"]})
|
|
plan = self.planner().plan(["src/Controls/Popup/Popup.cs"])
|
|
self.assertNotIn("Integration.Checks", self.selected(plan))
|
|
|
|
def test_explicit_rule_selects_integration_project_without_project_reference(self):
|
|
plan = self.planner().plan(["inputs/catalog.xlf"])
|
|
self.assertEqual({"Integration.Checks"}, self.selected(plan))
|
|
|
|
def test_test_helper_change_expands_to_its_entire_project(self):
|
|
self.write("tests/UI.Tests/TestHost.cs", "class TestHost {}")
|
|
plan = self.planner().plan(["tests/UI.Tests/TestHost.cs"])
|
|
self.assertEqual({"Tests.ButtonTests", "Tests.PopupTests", "Tests.SelectTests"}, self.selected(plan))
|
|
|
|
def test_changed_test_selects_actual_class_name_not_file_name(self):
|
|
plan = self.planner().plan(["tests/Integration.Tests/Checks.cs"])
|
|
self.assertEqual({"Integration.Checks"}, self.selected(plan))
|
|
|
|
def test_unknown_source_is_reported_instead_of_successfully_skipped(self):
|
|
plan = self.planner().plan(["src/NewPackage/Feature.cs"])
|
|
self.assertTrue(plan["gaps"])
|
|
|
|
def test_missing_rule_suite_is_a_gap_even_when_another_suite_matches(self):
|
|
self.policy["rules"][0]["tests"].append("tests/Missing.Tests/**/*.cs")
|
|
plan = self.planner().plan(["inputs/catalog.xlf"])
|
|
self.assertTrue(plan["gaps"])
|
|
|
|
def test_deleted_source_uses_baseline_symbols_to_find_consumers(self):
|
|
(self.root / "src/Controls/Popup/Popup.cs").unlink()
|
|
plan = self.planner().plan(["src/Controls/Popup/Popup.cs"])
|
|
self.assertIn("Tests.SelectTests", self.selected(plan))
|
|
|
|
def test_full_scope_discovers_projects_missing_from_solution(self):
|
|
self.write("AtomUI.slnx", "<Solution/>")
|
|
plan = self.planner().plan([], scope="full")
|
|
self.assertEqual(2, len(plan["tests"]))
|
|
self.assertIn("Integration.Checks", self.selected(plan))
|
|
|
|
def test_explicit_extra_tests_are_additive(self):
|
|
plan = self.planner().plan(["src/Controls/Button/Button.cs"], extra_tests=["tests/Integration.Tests/**/*.cs"])
|
|
self.assertEqual({"Tests.ButtonTests", "Integration.Checks"}, self.selected(plan))
|
|
|
|
def test_stale_plan_records_its_original_snapshot(self):
|
|
planner = self.planner()
|
|
plan = planner.plan(["src/Controls/Button/Button.cs"])
|
|
self.write("src/Controls/Select/Select.cs", "class Select { int changed; }")
|
|
self.assertNotEqual(planner.repo.snapshot(), plan.get("source_fingerprint", planner.repo.snapshot()))
|
|
|
|
def test_change_between_repository_discovery_and_planning_is_rejected(self):
|
|
self.planner()
|
|
repository = Repository(self.root)
|
|
self.write("tests/UI.Tests/AddedTests.cs", "namespace Tests; class AddedTests { [Fact] void Works() {} }")
|
|
with self.assertRaises(ValueError):
|
|
Planner(repository, self.policy)
|
|
|
|
def test_file_reading_test_is_selected_without_project_reference(self):
|
|
self.write("tests/Integration.Tests/Checks.cs", 'namespace Integration; class Checks { [Fact] void Works() { File.ReadAllText(Path.Combine("docs", "build-contract.md")); } }')
|
|
plan = self.planner().plan(["docs/build-contract.md"])
|
|
self.assertIn("Integration.Checks", self.selected(plan))
|
|
|
|
def test_inheritance_upgrade_propagates_after_composition_was_visited(self):
|
|
for name, body in [("BaseType", ""), ("Middle", ": BaseType"), ("Derived", ": Middle"), ("Consumer", "")]:
|
|
field = "BaseType item;" if name == "Derived" else "Derived item;" if name == "Consumer" else ""
|
|
self.write("src/Controls/" + name + "/" + name + ".cs", "class " + name + " " + body + " { " + field + " }")
|
|
self.write("tests/UI.Tests/" + name + "Tests.cs", "namespace Tests; class " + name + "Tests { [Fact] void Works() { new " + name + "(); } }")
|
|
plan = self.planner().plan(["src/Controls/BaseType/BaseType.cs"])
|
|
self.assertIn("Tests.ConsumerTests", self.selected(plan))
|
|
|
|
def test_runtime_source_reading_rules_take_precedence_over_document_ignores(self):
|
|
self.policy["rules"][0]["changes"] = ["docs/catalog/**"]
|
|
plan = self.planner().plan(["docs/catalog/overview.md"])
|
|
self.assertIn("Integration.Checks", self.selected(plan))
|
|
|
|
def test_publish_obligation_is_preserved_separately_from_unit_tests(self):
|
|
self.policy["obligations"] = [{"id": "aot", "changes": ["src/Controls/Popup/**"], "description": "Run real publish and startup"}]
|
|
plan = self.planner().plan(["src/Controls/Popup/Popup.cs"])
|
|
self.assertEqual(["aot"], [x["id"] for x in plan["obligations"]])
|
|
|
|
def test_audit_reports_unowned_production_file_and_dangling_rule(self):
|
|
self.write("src/Unknown/Other.cs", "class Other {}")
|
|
self.policy["rules"][0]["tests"] = ["tests/Absent/**/*.cs"]
|
|
audit = self.planner().audit()
|
|
self.assertTrue(any("src/Unknown/Other.cs" in x for x in audit["errors"]))
|
|
self.assertTrue(any("tests/Absent" in x for x in audit["errors"]))
|
|
|
|
|
|
class ChangeDiscoveryTests(RepositoryCase):
|
|
def test_staged_change_hidden_by_worktree_revert_is_still_reported(self):
|
|
self.planner()
|
|
original = (self.root / "src/Controls/Button/Button.cs").read_text()
|
|
self.write("src/Controls/Button/Button.cs", "class Button { int staged; }")
|
|
self.git("add", ".")
|
|
self.write("src/Controls/Button/Button.cs", original)
|
|
self.assertIn("src/Controls/Button/Button.cs", Repository(self.root).changes())
|
|
|
|
def test_staged_unstaged_untracked_and_both_rename_sides_are_included(self):
|
|
self.planner()
|
|
self.write("src/Controls/Button/Button.cs", "class Button { int staged; }")
|
|
self.git("add", ".")
|
|
self.write("src/Controls/Button/Button.cs", "class Button { int unstaged; }")
|
|
self.git("mv", "src/Controls/Popup/Popup.cs", "src/Controls/Popup/Renamed.cs")
|
|
self.write("src/Controls/Popup/New file.cs", "class NewPopup {}")
|
|
changes = Repository(self.root).changes()
|
|
self.assertTrue({"src/Controls/Button/Button.cs", "src/Controls/Popup/Popup.cs", "src/Controls/Popup/Renamed.cs", "src/Controls/Popup/New file.cs"}.issubset(set(changes)))
|
|
|
|
def test_explicit_base_includes_committed_and_working_changes(self):
|
|
self.planner()
|
|
base = self.git("rev-parse", "HEAD")
|
|
self.write("src/Controls/Button/Button.cs", "class Button { int changed; }")
|
|
self.git("add", ".")
|
|
self.git("commit", "-qm", "change")
|
|
self.write("src/Controls/Select/New.cs", "class NewSelect {}")
|
|
changes = Repository(self.root).changes(base)
|
|
self.assertIn("src/Controls/Button/Button.cs", changes)
|
|
self.assertIn("src/Controls/Select/New.cs", changes)
|
|
|
|
|
|
class ClassDiscoveryTests(unittest.TestCase):
|
|
def test_combined_attribute_list_discovers_fact(self):
|
|
self.assertIsNotNone(test_classes)
|
|
self.assertEqual(["Tests.Combined"], test_classes('namespace Tests; class Combined { [Trait("Area", "Controls"), Fact] public void Works() {} }'))
|
|
|
|
def test_partial_class_and_helper_are_not_inferred_from_filename(self):
|
|
self.assertIsNotNone(test_classes, "C# test discovery has not been implemented")
|
|
source = '''namespace Integration;
|
|
public sealed partial class EndToEndTests {
|
|
[Fact] public void Works() {}
|
|
public class Helper { public void Help() {} }
|
|
}
|
|
[CollectionDefinition("suite")] public class Definition {}
|
|
// [Fact] class FakeTests {}
|
|
'''
|
|
self.assertEqual(["Integration.EndToEndTests"], test_classes(source))
|
|
|
|
def test_strings_with_fake_tests_and_braces_do_not_change_class_scope(self):
|
|
self.assertIsNotNone(test_classes, "C# test discovery has not been implemented")
|
|
source = '''namespace Tests { public class Outer {
|
|
string sample = "class Fake { [Fact] }";
|
|
public class Inner { [AvaloniaFact] public void Works() {} }
|
|
}}'''
|
|
self.assertEqual(["Tests.Outer+Inner"], test_classes(source))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|