From b676b0a0652b2dcc7b287551f0d1419d617767b8 Mon Sep 17 00:00:00 2001 From: Matt Ownby Date: Tue, 20 Jan 2026 19:21:53 -0700 Subject: [PATCH 1/2] save mutation stats after printing them so a CI/CD pipeline can optionally take action on the results --- src/mutmut/__main__.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/mutmut/__main__.py b/src/mutmut/__main__.py index 4cad64d5..aa47c1f9 100644 --- a/src/mutmut/__main__.py +++ b/src/mutmut/__main__.py @@ -899,6 +899,20 @@ def save_stats(): stats_time=mutmut.stats_time, ), f, indent=4) +def save_cicd_stats(source_file_mutation_data_by_path): + s = calculate_summary_stats(source_file_mutation_data_by_path) + with open('mutants/mutmut-cicd-stats.json', 'w') as f: + json.dump(dict( + killed=s.killed, + survived=s.survived, + total=s.total, + no_tests=s.no_tests, + skipped=s.skipped, + suspicious=s.suspicious, + timeout=s.timeout, + check_was_interrupted_by_user=s.check_was_interrupted_by_user, + segfault=s.segfault + ), f, indent=4) def collect_source_file_mutation_data(*, mutant_names): source_file_mutation_data_by_path: Dict[str, SourceFileMutationData] = {} @@ -1152,6 +1166,9 @@ def read_one_child_exit_status(): print() print(f'{count_tried / t.total_seconds():.2f} mutations/second') + # save stats so CI/CD pipelines can optionally take action + save_cicd_stats(source_file_mutation_data_by_path) + if mutant_names: print() print('Mutant results') From 400c5dcec5f12fcc54e0d3da04f3c822a567425d Mon Sep 17 00:00:00 2001 From: Matt Ownby Date: Thu, 22 Jan 2026 18:18:23 -0700 Subject: [PATCH 2/2] added Click command to export cicd mutation data from a previous invocation of 'mutmut run' --- src/mutmut/__main__.py | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/mutmut/__main__.py b/src/mutmut/__main__.py index aa47c1f9..e982c617 100644 --- a/src/mutmut/__main__.py +++ b/src/mutmut/__main__.py @@ -914,6 +914,36 @@ def save_cicd_stats(source_file_mutation_data_by_path): segfault=s.segfault ), f, indent=4) +# exports CI/CD stats to block pull requests from merging if mutation score is too low, or used in other ways in CI/CD pipelines +@cli.command() +def export_cicd_stats(): + ensure_config_loaded() + + source_file_mutation_data_by_path: Dict[str, SourceFileMutationData] = {} + + for path in walk_source_files(): + if mutmut.config.should_ignore_for_mutation(path): + continue + + meta_path = Path('mutants') / (str(path) + '.meta') + if not meta_path.exists(): + continue + + m = SourceFileMutationData(path=path) + m.load() + if not m.exit_code_by_key: + continue + + source_file_mutation_data_by_path[str(path)] = m + + if not source_file_mutation_data_by_path: + print('No previous mutation data found. Run "mutmut run" first.') + return + + save_cicd_stats(source_file_mutation_data_by_path) + print('Saved CI/CD stats to mutants/mutmut-cicd-stats.json') + + def collect_source_file_mutation_data(*, mutant_names): source_file_mutation_data_by_path: Dict[str, SourceFileMutationData] = {} @@ -1166,9 +1196,6 @@ def read_one_child_exit_status(): print() print(f'{count_tried / t.total_seconds():.2f} mutations/second') - # save stats so CI/CD pipelines can optionally take action - save_cicd_stats(source_file_mutation_data_by_path) - if mutant_names: print() print('Mutant results')