@@ -223,6 +223,59 @@ def get_score(self) -> float:
223223 return self .num_killed_mutants / divisor
224224
225225
226+ def _select_minimal_assertions (
227+ kill_map : dict [tuple [int , int ], set [int ]],
228+ ) -> set [tuple [int , int ]]:
229+ """Greedy set-cover selection of a minimal assertion subset.
230+
231+ Given a mapping from assertion key ``(stmt_idx, assertion_idx)`` to the set of
232+ mutant indices that assertion kills, return the subset of keys that together
233+ cover every killed mutant (the union of all kill sets). Assertions with an empty
234+ kill set are never selected. Ties are broken by ascending key (insertion order)
235+ for deterministic output.
236+
237+ Args:
238+ kill_map: Mapping from assertion key to the set of mutants it kills.
239+
240+ Returns:
241+ The set of assertion keys to keep.
242+ """
243+ universe : set [int ] = set ()
244+ for kills in kill_map .values ():
245+ universe |= kills
246+
247+ # Only assertions that kill at least one mutant can ever be selected.
248+ candidates = {key : kills for key , kills in kill_map .items () if kills }
249+
250+ uncovered = set (universe )
251+ keep : set [tuple [int , int ]] = set ()
252+ while uncovered :
253+ best_key : tuple [int , int ] | None = None
254+ best_cover = 0
255+ for key in sorted (candidates ):
256+ cover = len (candidates [key ] & uncovered )
257+ if cover > best_cover :
258+ best_cover = cover
259+ best_key = key
260+ if best_key is None :
261+ break
262+ keep .add (best_key )
263+ uncovered -= candidates [best_key ]
264+ del candidates [best_key ]
265+
266+ # Greedy may leave a selection whose kills are fully covered by the others it
267+ # later picked. Prune such redundant assertions, dropping higher keys first so
268+ # lower (earlier) assertions are preferred, keeping coverage unchanged.
269+ for key in sorted (keep , reverse = True ):
270+ others : set [int ] = set ()
271+ for other in keep :
272+ if other != key :
273+ others |= kill_map [other ]
274+ if kill_map [key ] <= others :
275+ keep .discard (key )
276+ return keep
277+
278+
226279class MutationAnalysisAssertionGenerator (AssertionGenerator ):
227280 """Uses mutation analysis to filter out less relevant assertions."""
228281
@@ -381,6 +434,11 @@ def __remove_non_relevant_assertions(
381434 tests_mutants_results : list [list [ex .ExecutionResult | None ]],
382435 mutation_summary : _MutationSummary ,
383436 ) -> None :
437+ if config .configuration .test_case_output .assertion_minimization :
438+ MutationAnalysisAssertionGenerator .__minimize_assertions (
439+ test_cases , tests_mutants_results , mutation_summary
440+ )
441+ return
384442 for test , results in zip (test_cases , tests_mutants_results , strict = True ):
385443 merged = at .AssertionVerificationTrace ()
386444 for result , mut in zip (results , mutation_summary .mutant_information , strict = True ):
@@ -392,6 +450,77 @@ def __remove_non_relevant_assertions(
392450 if not merged .was_violated (stmt_idx , assertion_idx ):
393451 statement .assertions .remove (assertion )
394452
453+ @staticmethod
454+ def __minimize_assertions (
455+ test_cases : list [tc .TestCase ],
456+ tests_mutants_results : list [list [ex .ExecutionResult | None ]],
457+ mutation_summary : _MutationSummary ,
458+ ) -> None :
459+ """Keep a minimal subset of assertions preserving the killed mutants.
460+
461+ For each test case, a greedy set-cover selection keeps only enough
462+ assertions to preserve the full set of assertion-attributable mutant kills.
463+ Assertions that kill nothing are dropped (subsuming the plain
464+ non-relevant-assertion removal); redundant assertions that only re-kill
465+ mutants already covered by kept assertions are removed as well.
466+
467+ Statements carrying only an exception assertion are left untouched to
468+ avoid the fragility of mixing exception and value assertions.
469+
470+ Args:
471+ test_cases: The test cases whose assertions are minimized.
472+ tests_mutants_results: Per-test, per-mutant execution results.
473+ mutation_summary: Summary with per-mutant timeout information.
474+ """
475+ for test , results in zip (test_cases , tests_mutants_results , strict = True ):
476+ kill_map = MutationAnalysisAssertionGenerator .__build_kill_map (
477+ test , results , mutation_summary
478+ )
479+ keep = _select_minimal_assertions (kill_map )
480+
481+ for stmt_idx , statement in enumerate (test .statements ):
482+ if statement .has_only_exception_assertion ():
483+ continue
484+ for assertion_idx , assertion in reversed (list (enumerate (statement .assertions ))):
485+ if (stmt_idx , assertion_idx ) not in keep :
486+ statement .assertions .remove (assertion )
487+
488+ @staticmethod
489+ def __build_kill_map (
490+ test : tc .TestCase ,
491+ results : list [ex .ExecutionResult | None ],
492+ mutation_summary : _MutationSummary ,
493+ ) -> dict [tuple [int , int ], set [int ]]:
494+ """Map each assertion to the set of mutants it kills via violation.
495+
496+ Statements carrying only an exception assertion are skipped. Only
497+ assertion-attributable kills on non-timed-out mutants are counted.
498+
499+ Args:
500+ test: The test case whose assertions are inspected.
501+ results: Per-mutant execution results for this test.
502+ mutation_summary: Summary with per-mutant timeout information.
503+
504+ Returns:
505+ Mapping from ``(stmt_idx, assertion_idx)`` to the killed mutant indices.
506+ """
507+ kill_map : dict [tuple [int , int ], set [int ]] = {}
508+ for stmt_idx , statement in enumerate (test .statements ):
509+ if statement .has_only_exception_assertion ():
510+ continue
511+ for assertion_idx in range (len (statement .assertions )):
512+ kills = {
513+ mutant_idx
514+ for mutant_idx , (result , mut ) in enumerate (
515+ zip (results , mutation_summary .mutant_information , strict = True )
516+ )
517+ if result is not None
518+ and len (mut .timed_out_by ) == 0
519+ and result .assertion_verification_trace .was_violated (stmt_idx , assertion_idx )
520+ }
521+ kill_map [stmt_idx , assertion_idx ] = kills
522+ return kill_map
523+
395524 @staticmethod
396525 def __compute_mutation_summary (
397526 number_of_mutants : int ,
0 commit comments