| # Copyright 2026 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Tests for the solver.""" |
| |
| import json |
| import random |
| import unittest |
| |
| from ortools.sat.python import cp_model |
| |
| from pw_coverage.config_solver.model import ( |
| CoverageGoal, |
| Exclusion, |
| ExistingCoverage, |
| Option, |
| Parameter, |
| PointConstraint, |
| TestModel, |
| ) |
| from pw_coverage.config_solver.solver import ( |
| CoverageSolver, |
| SolverResult, |
| solve_model, |
| ) |
| |
| |
| class TestSolver(unittest.TestCase): |
| """Tests for the solver.""" |
| |
| def test_explicit_coverage(self) -> None: |
| # 2 Parameters, 2 Options each. |
| # Goal: Only cover parameter A (1-way). |
| # Should pick A1 and A2. B can be anything. |
| # Minimal solution size is 2 (e.g. {A1, B1}, {A2, B1}). |
| # If pairwise was implicit, it would require 4. |
| p_a = Parameter("A", [Option("1"), Option("2")]) |
| p_b = Parameter("B", [Option("1"), Option("2")]) |
| |
| model = TestModel([p_a, p_b], coverage_goals=[CoverageGoal(["A"])]) |
| |
| result = solve_model(model, verbose=False) |
| self.assertTrue(result.is_feasible) |
| self.assertEqual(len(result.selected_configurations), 2) |
| |
| # Verify A is fully covered |
| a_vals = {r["A"] for r in result.selected_configurations} |
| self.assertEqual(a_vals, {"1", "2"}) |
| |
| def test_simple_pairwise(self) -> None: |
| # 2 Parameters, 2 Options each. |
| # Explicitly ask for Pairwise. |
| p_a = Parameter("A", [Option("1"), Option("2")]) |
| p_b = Parameter("B", [Option("1"), Option("2")]) |
| |
| model = TestModel([p_a, p_b], coverage_goals=[CoverageGoal(["A", "B"])]) |
| |
| result = solve_model(model, verbose=False) |
| self.assertTrue(result.is_feasible) |
| # Should select all 4 because we need (A1, B1), (A1, B2), (A2, B1), |
| # (A2, B2) |
| self.assertEqual(len(result.selected_configurations), 4) |
| |
| def test_cost_minimization(self) -> None: |
| # A1 is cheap, A2 is expensive. |
| # B1 is cheap. |
| # We need (A1, B1) and (A2, B1). |
| p_a = Parameter("A", [Option("1", cost=1), Option("2", cost=100)]) |
| p_b = Parameter("B", [Option("1", cost=1)]) |
| |
| # Explicit goal: Cover A x B |
| model = TestModel([p_a, p_b], coverage_goals=[CoverageGoal(["A", "B"])]) |
| |
| result = solve_model(model, verbose=False) |
| self.assertEqual(len(result.selected_configurations), 2) |
| |
| costs = sum( |
| sum( |
| o.cost |
| for p in model.parameters |
| for o in p.options |
| if o.value == r[p.name] |
| ) |
| for r in result.selected_configurations |
| ) |
| # Cost should be (1+1) + (100+1) = 103 |
| self.assertEqual(costs, 103) |
| |
| def test_unsatisfiable_model(self) -> None: |
| # A model where NO configuration is valid. |
| p_a = Parameter("A", [Option("1")]) |
| # Constraint: IF A=1 THEN A!=1 (Impossible) |
| ex = Exclusion({"A": "1"}, {"A": {"1"}}) |
| |
| # Goal: Cover A |
| model = TestModel( |
| [p_a], constraints=[ex], coverage_goals=[CoverageGoal(["A"])] |
| ) |
| result = solve_model(model, verbose=False) |
| self.assertFalse(result.is_feasible) |
| |
| def test_unsatisfiable_model_verbose(self) -> None: |
| """Ensure verbose logging doesn't crash on failure.""" |
| p_a = Parameter("A", [Option("1")]) |
| ex = Exclusion({"A": "1"}, {"A": {"1"}}) |
| model = TestModel( |
| [p_a], constraints=[ex], coverage_goals=[CoverageGoal(["A"])] |
| ) |
| |
| # This will print error messages to stdout, which we don't strictly |
| # assert on, but we ensure it doesn't raise an exception. |
| result = solve_model(model, verbose=True) |
| self.assertFalse(result.is_feasible) |
| |
| def test_uncoverable_pair_handled_gracefully(self) -> None: |
| # A, B. (A1, B1) is invalid. |
| # Solver should pick enough configs to cover (A1, B2), (A2, B1), |
| # (A2, B2). It should NOT fail just because (A1, B1) is impossible. |
| p_a = Parameter("A", [Option("1"), Option("2")]) |
| p_b = Parameter("B", [Option("1"), Option("2")]) |
| |
| ex = Exclusion({"A": "1"}, {"B": {"1"}}) |
| model = TestModel( |
| [p_a, p_b], [ex], coverage_goals=[CoverageGoal(["A", "B"])] |
| ) |
| |
| result = solve_model(model, verbose=False) |
| self.assertTrue(result.is_feasible) |
| |
| # Verify (A1, B1) is NOT in result |
| for r in result.selected_configurations: |
| self.assertFalse(r["A"] == "1" and r["B"] == "1") |
| |
| # Verify valid pairs ARE covered |
| covered = set() |
| for r in result.selected_configurations: |
| covered.add((r["A"], r["B"])) |
| |
| self.assertIn(("1", "2"), covered) |
| self.assertIn(("2", "1"), covered) |
| self.assertIn(("2", "2"), covered) |
| |
| def test_optimization_avoids_expensive_redundant_config(self) -> None: |
| # 3 Parameters: A, B, C with options 1, 2. |
| # Config {A1, B1, C1} is extremely expensive. |
| # Its pairs (A1, B1), (A1, C1), (B1, C1) can be covered by other |
| # configs: |
| # - (A1, B1) covered by {A1, B1, C2} |
| # - (A1, C1) covered by {A1, B2, C1} |
| # - (B1, C1) covered by {A2, B1, C1} |
| |
| p_a = Parameter("A", [Option("1"), Option("2")]) |
| p_b = Parameter("B", [Option("1"), Option("2")]) |
| p_c = Parameter("C", [Option("1"), Option("2")]) |
| |
| # Explicit Full 2-way coverage goal |
| model = TestModel( |
| [p_a, p_b, p_c], |
| coverage_goals=[ |
| CoverageGoal(["A", "B"]), |
| CoverageGoal(["A", "C"]), |
| CoverageGoal(["B", "C"]), |
| ], |
| ) |
| |
| result = solve_model(model, verbose=False) |
| self.assertTrue(result.is_feasible) |
| |
| # Cartesian is 8. Minimal covering array is 4. |
| # Our solver should find 4. |
| self.assertEqual(len(result.selected_configurations), 4) |
| |
| def test_optimization_with_costs(self) -> None: |
| # A1 cost 100, A2 cost 1. |
| # We want to minimize occurrences of A1 while still covering validation. |
| # 3 params x 2 opts. |
| # We need 4 configs total. |
| p_a = Parameter("A", [Option("1", cost=100), Option("2", cost=1)]) |
| p_b = Parameter("B", [Option("1", cost=1), Option("2", cost=1)]) |
| p_c = Parameter("C", [Option("1", cost=1), Option("2", cost=1)]) |
| |
| model = TestModel( |
| [p_a, p_b, p_c], |
| coverage_goals=[ |
| CoverageGoal(["A", "B"]), |
| CoverageGoal(["A", "C"]), |
| CoverageGoal(["B", "C"]), |
| ], |
| ) |
| result = solve_model(model, verbose=False) |
| self.assertTrue(result.is_feasible) |
| |
| # Count occurrences of A1 |
| a1_count = sum( |
| 1 for r in result.selected_configurations if r["A"] == "1" |
| ) |
| self.assertEqual(a1_count, 2) |
| |
| def test_existing_coverage_reduce_solution_size(self) -> None: |
| # 2 Parameters, 2 Options each. |
| p_a = Parameter("A", [Option("1"), Option("2")]) |
| p_b = Parameter("B", [Option("1"), Option("2")]) |
| # Explicit Full 2-way coverage goal (needs 4 configs) |
| # We provide 2 existing configs covering (A1, B1) and (A2, B2) |
| model = TestModel( |
| [p_a, p_b], |
| coverage_goals=[CoverageGoal(["A", "B"])], |
| existing_coverage=[{"A": "1", "B": "1"}, {"A": "2", "B": "2"}], |
| ) |
| |
| result = solve_model(model, verbose=False) |
| self.assertTrue(result.is_feasible) |
| # We need to cover the remaining: (A1, B2) and (A2, B1). |
| # Should be exactly 2 new configs. |
| self.assertEqual(len(result.selected_configurations), 2) |
| |
| # Verify the new configs cover the gaps |
| new_interactions = set( |
| (r["A"], r["B"]) for r in result.selected_configurations |
| ) |
| self.assertIn(("1", "2"), new_interactions) |
| self.assertIn(("2", "1"), new_interactions) |
| |
| def test_existing_coverage_full(self) -> None: |
| # Provide all 4 configs as existing coverage. Solver should return |
| # empty list. |
| p_a = Parameter("A", [Option("1"), Option("2")]) |
| p_b = Parameter("B", [Option("1"), Option("2")]) |
| existing = [ |
| {"A": "1", "B": "1"}, |
| {"A": "1", "B": "2"}, |
| {"A": "2", "B": "1"}, |
| {"A": "2", "B": "2"}, |
| ] |
| model = TestModel( |
| [p_a, p_b], |
| coverage_goals=[CoverageGoal(["A", "B"])], |
| existing_coverage=existing, |
| ) |
| |
| result = solve_model(model, verbose=False) |
| self.assertTrue(result.is_feasible) |
| self.assertEqual(len(result.selected_configurations), 0) |
| |
| def test_include_existing_combines_existing_and_new(self) -> None: |
| p_a = Parameter("A", [Option("1"), Option("2")]) |
| p_b = Parameter("B", [Option("1"), Option("2")]) |
| existing = [{"A": "1", "B": "1"}, {"A": "2", "B": "2"}] |
| model = TestModel( |
| [p_a, p_b], |
| coverage_goals=[CoverageGoal(["A", "B"])], |
| existing_coverage=existing, |
| ) |
| |
| result = CoverageSolver(model, verbose=False).solve( |
| include_existing=True |
| ) |
| self.assertTrue(result.is_feasible) |
| self.assertEqual(len(result.selected_configurations), 4) |
| # First 2 configs should be the existing ones in order |
| self.assertEqual(result.selected_configurations[:2], existing) |
| # Remaining 2 configs cover the gaps |
| new_interactions = set( |
| (r["A"], r["B"]) for r in result.selected_configurations[2:] |
| ) |
| self.assertEqual(new_interactions, {("1", "2"), ("2", "1")}) |
| |
| def test_include_existing_full_coverage(self) -> None: |
| p_a = Parameter("A", [Option("1"), Option("2")]) |
| p_b = Parameter("B", [Option("1"), Option("2")]) |
| existing = [ |
| {"A": "1", "B": "1"}, |
| {"A": "1", "B": "2"}, |
| {"A": "2", "B": "1"}, |
| {"A": "2", "B": "2"}, |
| ] |
| model = TestModel( |
| [p_a, p_b], |
| coverage_goals=[CoverageGoal(["A", "B"])], |
| existing_coverage=existing, |
| ) |
| |
| result = solve_model(model, verbose=False, include_existing=True) |
| self.assertTrue(result.is_feasible) |
| self.assertEqual(result.selected_configurations, existing) |
| |
| def test_include_existing_no_goals(self) -> None: |
| p_a = Parameter("A", [Option("1"), Option("2")]) |
| existing = [{"A": "1"}] |
| model = TestModel( |
| [p_a], |
| existing_coverage=existing, |
| ) |
| |
| result = CoverageSolver(model, verbose=False).solve( |
| include_existing=True |
| ) |
| self.assertTrue(result.is_feasible) |
| self.assertEqual(result.selected_configurations, existing) |
| |
| def test_include_existing_partial_raises(self) -> None: |
| p_a = Parameter("A", [Option("1"), Option("2")]) |
| p_b = Parameter("B", [Option("1"), Option("2")]) |
| model = TestModel( |
| [p_a, p_b], |
| coverage_goals=[CoverageGoal(["A"])], |
| existing_coverage=[ExistingCoverage({"A": "1"}, partial=True)], |
| ) |
| |
| solver = CoverageSolver(model, verbose=False) |
| with self.assertRaisesRegex( |
| ValueError, "Cannot use include_existing=True with partial" |
| ): |
| solver.solve(include_existing=True) |
| |
| def test_existing_coverage_robustness_partial(self) -> None: |
| """Partial test of existing coverage robustness.""" |
| # Verify an existing config that only covers SOME params MUST be |
| # wrapped in ExistingCoverage(..., partial=True). |
| p_a = Parameter("A", [Option("1"), Option("2")]) |
| p_b = Parameter("B", [Option("1"), Option("2")]) |
| p_c = Parameter("C", [Option("1"), Option("2")]) |
| |
| # Attempting to pass a partial dict without "C" should now FAIL |
| # validation. |
| with self.assertRaisesRegex(ValueError, "missing parameters"): |
| TestModel( |
| [p_a, p_b, p_c], |
| coverage_goals=[CoverageGoal(["A", "C"])], |
| existing_coverage=[{"A": "1", "B": "1"}], |
| ) |
| |
| # But if we mark it partial=True, it should work. |
| model = TestModel( |
| [p_a, p_b, p_c], |
| coverage_goals=[CoverageGoal(["A", "C"])], |
| existing_coverage=[ |
| ExistingCoverage({"A": "1", "B": "1"}, partial=True) |
| ], |
| ) |
| |
| result = solve_model(model, verbose=False) |
| self.assertTrue(result.is_feasible) |
| # Existing config doesn't satisfy A x C (missing C) |
| # We need 4 configs for A x C. Existing config covers NONE of them |
| # because C is missing. |
| self.assertEqual(len(result.selected_configurations), 4) |
| |
| def test_existing_coverage_robustness_duplicates(self) -> None: |
| # Verify duplicate existing configs don't crash or double-count |
| # inappropriately. |
| p_a = Parameter("A", [Option("1"), Option("2")]) |
| existing = [{"A": "1"}, {"A": "1"}] |
| model = TestModel( |
| [p_a], |
| coverage_goals=[CoverageGoal(["A"])], |
| existing_coverage=existing, |
| ) |
| result = solve_model(model, verbose=False) |
| # Should just pick A2. |
| self.assertTrue(result.is_feasible) |
| self.assertEqual(len(result.selected_configurations), 1) |
| self.assertEqual(result.selected_configurations[0]["A"], "2") |
| |
| def test_existing_coverage_robustness_invalid_strict(self) -> None: |
| # Verify an existing config that violates constraints IS NOW REJECTED. |
| p_a = Parameter("A", [Option("1"), Option("2")]) |
| p_b = Parameter("B", [Option("1"), Option("2")]) |
| |
| # Constraint: A=1 IMPLIES B!=1 (so A1, B1 is invalid) |
| ex = Exclusion({"A": "1"}, {"B": {"1"}}) |
| |
| # We provide the invalid config. Should raise ValueError. |
| invalid_existing = [{"A": "1", "B": "1"}] |
| with self.assertRaisesRegex(ValueError, "violates model constraints"): |
| TestModel( |
| [p_a, p_b], |
| [ex], |
| coverage_goals=[CoverageGoal(["A", "B"])], |
| existing_coverage=invalid_existing, |
| ) |
| |
| def test_point_constraint_basic(self) -> None: |
| # Ensure we can force a specific partial config |
| p_a = Parameter("A", [Option("1"), Option("2")]) |
| p_b = Parameter("B", [Option("1"), Option("2")]) |
| |
| # We want A=1 to be selected, even if not required by coverage goals |
| # No coverage goals, just a point constraint |
| pc = PointConstraint({"A": "1"}) |
| model = TestModel([p_a, p_b], point_constraints=[pc]) |
| |
| result = solve_model(model, verbose=False) |
| self.assertTrue(result.is_feasible) |
| self.assertTrue( |
| any(c["A"] == "1" for c in result.selected_configurations) |
| ) |
| |
| def test_point_constraint_constructive_generation(self) -> None: |
| # Create a scenario where random sampling is unlikely to find the target |
| # But we force it via PointConstraint |
| |
| # 3 params with 10 options each = 1000 combinations |
| # We only take max_candidates=10 (very small sample) |
| # It's unlikely to hit A=9, B=9, C=9 by pure chance |
| opts = [Option(str(i)) for i in range(10)] |
| p_a = Parameter("A", opts) |
| p_b = Parameter("B", opts) |
| p_c = Parameter("C", opts) |
| |
| pc = PointConstraint({"A": "9", "B": "9", "C": "9"}) |
| model = TestModel([p_a, p_b, p_c], point_constraints=[pc]) |
| |
| # Force small sample size to ensure we rely on constructive gen |
| result = solve_model(model, verbose=False, max_candidates=5) |
| |
| self.assertTrue(result.is_feasible) |
| found = False |
| for c in result.selected_configurations: |
| if c["A"] == "9" and c["B"] == "9" and c["C"] == "9": |
| found = True |
| break |
| self.assertTrue(found, "Did not find forced PointConstraint in results") |
| |
| def test_point_constraint_conflict_infeasible(self) -> None: |
| # Constraint conflict -> Infeasible |
| p_a = Parameter("A", [Option("1")]) |
| # Exclusion: A=1 is forbidden |
| ex = Exclusion({"A": "1"}, {"A": {"1"}}) |
| # PointConstraint: Force A=1 |
| pc = PointConstraint({"A": "1"}) |
| |
| model = TestModel([p_a], constraints=[ex], point_constraints=[pc]) |
| |
| # Should fail to generate candidate, leading to infeasible model |
| result = solve_model(model, verbose=False) |
| self.assertFalse(result.is_feasible) |
| |
| def test_solver_result_stats(self) -> None: |
| # 2 Parameters, 2 Options each. |
| p_a = Parameter("A", [Option("1"), Option("2")]) |
| p_b = Parameter("B", [Option("1"), Option("2")]) |
| |
| # Goal: Cover A x B (4 combinations) |
| # Existing: Covers (A1, B1) |
| model = TestModel( |
| [p_a, p_b], |
| coverage_goals=[CoverageGoal(["A", "B"])], |
| existing_coverage=[ExistingCoverage({"A": "1", "B": "1"})], |
| ) |
| |
| result = solve_model(model, verbose=False) |
| self.assertTrue(result.is_feasible) |
| |
| # Total requirements: 4 pairs |
| self.assertEqual(result.total_requirements, 4) |
| |
| # Covered by existing: 1 pair (A1, B1) |
| self.assertEqual(result.requirements_covered_by_existing, 1) |
| |
| # Covered by solver: 3 pairs (A1, B2), (A2, B1), (A2, B2) |
| # Note: Valid configurations generated by solver will cover these. |
| self.assertEqual(result.requirements_covered_by_solver, 3) |
| |
| def test_solver_result_to_json(self) -> None: |
| result = SolverResult( |
| status=cp_model.OPTIMAL, |
| objective_value=10.0, |
| time_taken=0.123, |
| selected_configurations=[{"OS": "Linux", "Compiler": "Clang"}], |
| total_requirements=4, |
| requirements_covered_by_existing=1, |
| requirements_covered_by_solver=3, |
| solver_num_variables=10, |
| solver_num_constraints=5, |
| solver_num_literals=8, |
| ) |
| |
| expected = { |
| "status": "OPTIMAL", |
| "is_optimal": True, |
| "is_feasible": True, |
| "objective_value": 10.0, |
| "time_taken": 0.123, |
| "selected_configurations": [{"OS": "Linux", "Compiler": "Clang"}], |
| "total_requirements": 4, |
| "requirements_covered_by_existing": 1, |
| "requirements_covered_by_solver": 3, |
| "solver_num_variables": 10, |
| "solver_num_constraints": 5, |
| "solver_num_literals": 8, |
| } |
| |
| self.assertEqual(result.to_json(), expected) |
| serialized = json.dumps(result.to_json()) |
| self.assertIsInstance(serialized, str) |
| self.assertEqual(json.loads(serialized), expected) |
| |
| |
| class TestCoverageSolverInternals(unittest.TestCase): |
| """Tests for the internals of the solver.""" |
| |
| # pylint: disable=protected-access |
| |
| def test_generate_candidate_pool_enumeration(self) -> None: |
| # Small domain: 2x2 = 4 candidates. Max = 10. Should enumerate. |
| p_a = Parameter("A", [Option("1"), Option("2")]) |
| p_b = Parameter("B", [Option("1"), Option("2")]) |
| model = TestModel([p_a, p_b]) |
| |
| solver = CoverageSolver(model, verbose=False, max_candidates=10) |
| configs, costs = solver._generate_candidate_pool(random.Random(123)) |
| |
| self.assertEqual(len(configs), 4) |
| # Check costs: all 0? No defaults are 1? |
| # Wait, Option defaults cost to ?? dataclass defaults? |
| # Option definition: Option(value, cost=1) |
| # So cost should be 2 for each. |
| self.assertEqual(len(costs), 4) |
| self.assertTrue( |
| all(c == 2 for c in costs), |
| "Default cost is 1 per option, so 2 params = 2", |
| ) |
| # Actually let's restrict cost check or check carefully. |
| # solver.py:27: Option("Linux", cost=10) |
| # Let's check Option default. |
| |
| def test_generate_candidate_pool_sampling(self) -> None: |
| # Large domain: 10 options x 10 options x 10 options = 1000. |
| # Max candidates = 50. Should sample. |
| opts = [Option(str(i)) for i in range(10)] |
| model = TestModel( |
| [Parameter("A", opts), Parameter("B", opts), Parameter("C", opts)] |
| ) |
| |
| solver = CoverageSolver(model, verbose=False, max_candidates=50) |
| configs, _ = solver._generate_candidate_pool( |
| rng=random.Random(123), |
| ) |
| |
| self.assertEqual(len(configs), 50) |
| |
| def test_generate_candidate_pool_exclusions(self) -> None: |
| p_a = Parameter("A", [Option("1"), Option("2")]) |
| # Exclusion: A=1 Forbidden. |
| ex = Exclusion({"A": "1"}, {"A": {"1"}}) # Self-exclusion |
| model = TestModel([p_a], constraints=[ex]) |
| |
| solver = CoverageSolver(model, verbose=False) |
| configs, _ = solver._generate_candidate_pool(random.Random(123)) |
| |
| # Should only have A=2 |
| self.assertTrue(all(c["A"] == "2" for c in configs)) |
| |
| def test_enforce_point_constraints_adds_missing(self) -> None: |
| # A=1..10. Sample size 2. Unlikely to hit A=5. |
| # PointConstraint A=5. |
| opts = [Option(str(i)) for i in range(10)] |
| model = TestModel( |
| [Parameter("A", opts)], |
| point_constraints=[PointConstraint({"A": "5"})], |
| ) |
| |
| solver = CoverageSolver(model, verbose=False, max_candidates=2) |
| |
| # Manually inject into the pool with something irrelevant |
| pool = [{"A": "0"}, {"A": "1"}] |
| costs = [1, 1] |
| |
| solver._enforce_point_constraints(pool, costs, random.Random(123)) |
| |
| self.assertTrue(any(c["A"] == "5" for c in pool)) |
| self.assertGreater(len(pool), 2) |
| |
| def test_goal_driven_generation_finds_needle(self) -> None: |
| """Test that goal driven generation finds the needle.""" |
| # Scenario: Large domain, small max_candidates. |
| # We have a goal (A, B). |
| # We make (A=9, B=9) valid ONLY if C=9. |
| # Random sampling is unlikely to hit (A=9, B=9, C=9) if domain is large. |
| # Goal-driven should iterate (A=9, B=9), try to complete it, find C=9 |
| # is needed, and succeed. |
| |
| # 3 params, 20 options each => 8000 combinations. |
| opts = [Option(str(i)) for i in range(20)] |
| p_a = Parameter("A", opts) |
| p_b = Parameter("B", opts) |
| p_c = Parameter("C", opts) |
| |
| # Constraint: IF A=19 AND B=19 THEN C MUST BE 19. |
| # Actually easier: Make everything invalid EXCEPT A=19,B=19,C=19? No, |
| # that makes pool empty. |
| # Let's say we want to cover (A, B). |
| # Random sampling will fill pool with random stuff. |
| # We want to ensure (A=19, B=19) is covered. |
| # We can make (A=19, B=19) only valid with C=19 to make it "hard" to |
| # construct randomly? |
| # Or just rely on statistics: 8000 combos. Sample 10. Prob of hitting |
| # (19, 19, X) is low. |
| |
| model = TestModel( |
| [p_a, p_b, p_c], coverage_goals=[CoverageGoal(["A", "B"])] |
| ) |
| |
| # Use very small max_candidates to disable full enumeration and rely on |
| # sampling/goal-driven |
| # Domain 8000 > 10. |
| solver = CoverageSolver(model, verbose=False, max_candidates=10) |
| |
| # We expect Goal-Driven to generate candidates for ALL 20x20=400 pairs |
| # of (A, B). |
| # One of them will be (A=19, B=19). |
| # So the pool should contain at least 400 candidates? |
| # Wait, if we generate 400 goal candidates, we exceed max_candidates=10. |
| # That's fine, logic says valid_configs.extend(goal_configs) then fill |
| # remainder. |
| # So we should end up with >= 400 candidates. |
| |
| configs, _ = solver._generate_candidate_pool(random.Random(123)) |
| |
| self.assertGreaterEqual(len(configs), 400) |
| |
| # Verify (A=19, B=19) is present |
| found = any(c["A"] == "19" and c["B"] == "19" for c in configs) |
| self.assertTrue( |
| found, |
| "Goal-driven generation failed to produce specific target pair", |
| ) |
| |
| def test_identify_requirements(self) -> None: |
| p_a = Parameter("A", [Option("1")]) |
| p_b = Parameter("B", [Option("2")]) |
| model = TestModel([p_a, p_b], coverage_goals=[CoverageGoal(["A", "B"])]) |
| |
| solver = CoverageSolver(model, verbose=False) |
| pool = [{"A": "1", "B": "2"}] |
| |
| reqs = solver._identify_requirements(pool) |
| self.assertEqual(len(reqs), 1) |
| self.assertIn((("A", "B"), ("1", "2")), reqs) |
| |
| def test_prune_covered_requirements(self) -> None: |
| p_a = Parameter("A", [Option("1")]) |
| model = TestModel([p_a], existing_coverage=[{"A": "1"}]) |
| |
| solver = CoverageSolver(model, verbose=False) |
| # Requirement: A=1 |
| reqs = {(("A",), ("1",))} |
| |
| solver._prune_covered_requirements(reqs) |
| self.assertEqual(len(reqs), 0) |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |