1use fallow_types::envelope::{ElapsedMs, ToolVersion};
10use serde::{Deserialize, Serialize};
11
12use crate::audit_test_reach::TestReachClass;
13use crate::root_envelopes::serialize_named_json_output;
14
15pub const MUTATION_SCHEMA_VERSION: u32 = 1;
17pub const MUTATION_PLAN_SCHEMA_VERSION: u32 = 1;
19pub const MUTATION_INSPECT_SCHEMA_VERSION: u32 = 1;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
24#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
25pub enum MutationSchemaVersion {
26 #[serde(rename = "1")]
28 V1,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
33#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
34pub enum MutationPlanSchemaVersion {
35 #[serde(rename = "1")]
37 V1,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
42#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
43#[serde(rename_all = "snake_case")]
44pub enum MutantStatus {
45 Killed,
47 Timeout,
51 Survived,
54 NoCoverage,
56 RuntimeError,
58 Flaky,
61 Pruned,
63 Pending,
65 Equivalent,
68 CompileError,
72}
73
74#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
76#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
77pub struct MutationStatusCounts {
78 pub total: usize,
80 pub killed: usize,
82 pub timeout: usize,
84 pub survived: usize,
86 pub no_coverage: usize,
88 pub runtime_error: usize,
90 pub flaky: usize,
93 pub pruned: usize,
95 pub pending: usize,
97 pub equivalent: usize,
99 #[serde(default)]
102 pub compile_error: usize,
103}
104
105impl MutationStatusCounts {
106 #[must_use]
109 pub const fn executed(&self) -> usize {
110 self.killed
111 + self.timeout
112 + self.survived
113 + self.runtime_error
114 + self.flaky
115 + self.compile_error
116 }
117
118 pub const fn add(&mut self, status: MutantStatus) {
120 self.total += 1;
121 match status {
122 MutantStatus::Killed => self.killed += 1,
123 MutantStatus::Timeout => self.timeout += 1,
124 MutantStatus::Survived => self.survived += 1,
125 MutantStatus::NoCoverage => self.no_coverage += 1,
126 MutantStatus::RuntimeError => self.runtime_error += 1,
127 MutantStatus::Flaky => self.flaky += 1,
128 MutantStatus::Pruned => self.pruned += 1,
129 MutantStatus::Pending => self.pending += 1,
130 MutantStatus::Equivalent => self.equivalent += 1,
131 MutantStatus::CompileError => self.compile_error += 1,
132 }
133 }
134
135 pub const fn remove(&mut self, status: MutantStatus) {
137 let count = match status {
138 MutantStatus::Killed => &mut self.killed,
139 MutantStatus::Timeout => &mut self.timeout,
140 MutantStatus::Survived => &mut self.survived,
141 MutantStatus::NoCoverage => &mut self.no_coverage,
142 MutantStatus::RuntimeError => &mut self.runtime_error,
143 MutantStatus::Flaky => &mut self.flaky,
144 MutantStatus::Pruned => &mut self.pruned,
145 MutantStatus::Pending => &mut self.pending,
146 MutantStatus::Equivalent => &mut self.equivalent,
147 MutantStatus::CompileError => &mut self.compile_error,
148 };
149 if *count > 0 {
150 *count -= 1;
151 self.total -= 1;
152 }
153 }
154
155 #[must_use]
163 #[expect(
164 clippy::cast_precision_loss,
165 reason = "mutant counts are far below 2^52"
166 )]
167 pub fn score(&self) -> Option<f64> {
168 let detected = self.killed + self.timeout;
169 let executed = detected + self.survived;
170 (executed > 0).then(|| detected as f64 / executed as f64)
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
176#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
177#[serde(rename_all = "snake_case")]
178pub enum MutationCompletionStatus {
179 Complete,
182 Partial,
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
188#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
189#[serde(rename_all = "kebab-case")]
190pub enum MutationIncompleteReason {
191 RuntimeError,
193 MutantLimit,
195 TimeBudget,
197 SkippedFiles,
199 UnloadedFiles,
202 TestFilesNotDiscovered,
207 BaselineFailure,
209 WorktreeChanged,
211 TypecheckFailed,
214 WorktreeUnchecked,
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
220#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
221pub struct MutationCompletion {
222 pub status: MutationCompletionStatus,
224 pub reasons: Vec<MutationIncompleteReason>,
226}
227
228impl MutationCompletion {
229 #[must_use]
232 pub fn from_reasons(mut reasons: Vec<MutationIncompleteReason>) -> Self {
233 reasons.sort_unstable();
234 reasons.dedup();
235 let status = if reasons.is_empty() {
236 MutationCompletionStatus::Complete
237 } else {
238 MutationCompletionStatus::Partial
239 };
240 Self { status, reasons }
241 }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
246#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
247#[serde(rename_all = "snake_case")]
248pub enum MutationScopeKind {
249 Changed,
251 All,
253 Paths,
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
259#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
260pub struct MutationScopeOutput {
261 pub kind: MutationScopeKind,
263 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub base_ref: Option<String>,
266}
267
268#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
270#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
271pub struct MutationRuntimeOutput {
272 pub name: String,
274 pub version: String,
276}
277
278#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
280#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
281pub struct MutationRunnerOutput {
282 pub name: String,
284 pub version: String,
286 pub protocol_version: u32,
288 pub runtime: MutationRuntimeOutput,
290 pub concurrency: usize,
292 #[serde(default, skip_serializing_if = "Vec::is_empty")]
297 pub notes: Vec<String>,
298}
299
300#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
302#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
303pub struct MutantLocation {
304 pub mutant_id: String,
306 pub file: String,
308 pub line: u32,
310 pub column: u32,
312 pub span: (u32, u32),
314 pub operator: String,
316 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub function: Option<String>,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
321 pub function_line: Option<u32>,
322 #[serde(default, skip_serializing_if = "Option::is_none")]
324 pub rank: Option<u32>,
325 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub change: Option<MutantChange>,
329}
330
331#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
339#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
340pub struct MutantChange {
341 pub from: String,
343 pub to: String,
345}
346
347#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
349#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
350pub struct MutantResult {
351 #[serde(flatten)]
353 pub location: MutantLocation,
354 pub status: MutantStatus,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
358 pub prune_reason: Option<String>,
359 #[serde(default, skip_serializing_if = "Option::is_none")]
370 pub killed_by: Option<String>,
371 #[serde(default, skip_serializing_if = "Vec::is_empty")]
373 pub covering_tests: Vec<String>,
374 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
376 pub static_mutant: bool,
377 #[serde(default, skip_serializing_if = "Option::is_none")]
380 pub duration_ms: Option<u64>,
381 #[serde(default, skip_serializing_if = "Option::is_none")]
384 pub test_reach: Option<MutantTestReach>,
385 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
389 pub retried: bool,
390 #[serde(default, skip_serializing_if = "Vec::is_empty")]
395 pub attempts: Vec<MutantAttempt>,
396 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
399 pub cached: bool,
400 #[serde(default, skip_serializing_if = "Vec::is_empty")]
403 pub type_error_codes: Vec<u32>,
404}
405
406#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
408#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
409pub struct MutantAttempt {
410 pub status: MutantStatus,
412 #[serde(default, skip_serializing_if = "Option::is_none")]
415 pub duration_ms: Option<u64>,
416 #[serde(default, skip_serializing_if = "Option::is_none")]
418 pub killed_by: Option<String>,
419 #[serde(default, skip_serializing_if = "Option::is_none")]
422 pub failure_kind: Option<MutantFailureKind>,
423 #[serde(default, skip_serializing_if = "Option::is_none")]
425 pub detail: Option<String>,
426}
427
428#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
430#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
431#[serde(rename_all = "snake_case")]
432pub enum MutantFailureKind {
433 Assertion,
435 Error,
437 Timeout,
439}
440
441impl MutantFailureKind {
442 #[must_use]
445 pub fn from_wire(value: &str) -> Option<Self> {
446 match value {
447 "assertion" => Some(Self::Assertion),
448 "error" => Some(Self::Error),
449 "timeout" => Some(Self::Timeout),
450 _ => None,
451 }
452 }
453}
454
455#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
464#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
465pub struct MutantTestReach {
466 pub reach: TestReachClass,
468 #[serde(default, skip_serializing_if = "Option::is_none")]
471 pub export_name: Option<String>,
472 #[serde(default, skip_serializing_if = "Vec::is_empty")]
475 pub tests: Vec<String>,
476 #[serde(default, skip_serializing_if = "Vec::is_empty")]
479 pub mocked_targets: Vec<String>,
480}
481
482#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
484#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
485pub struct MutationCacheOutput {
486 pub hits: usize,
488 pub stored: usize,
490 pub not_keyed: Vec<String>,
495}
496
497#[derive(Debug, Clone, PartialEq, Serialize)]
499#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
500pub struct MutationFunctionResult {
501 pub file: String,
503 pub name: Option<String>,
506 pub line: Option<u32>,
508 pub end_line: Option<u32>,
510 pub in_diff: bool,
512 #[serde(default, skip_serializing_if = "Option::is_none")]
514 pub cyclomatic: Option<u16>,
515 pub counts: MutationStatusCounts,
517 pub score: Option<f64>,
519 #[serde(default, skip_serializing_if = "Vec::is_empty")]
521 pub survived: Vec<String>,
522}
523
524#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
526#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
527pub struct MutationSummary {
528 pub counts: MutationStatusCounts,
530 pub score: Option<f64>,
535 pub coverage: Option<f64>,
543}
544
545impl MutationSummary {
546 #[must_use]
548 pub fn from_results(results: &[MutantResult]) -> Self {
549 let mut counts = MutationStatusCounts::default();
550 let (mut covered, mut uncovered) = (0usize, 0usize);
551 for result in results {
552 counts.add(result.status);
553 match result.status {
554 MutantStatus::NoCoverage => uncovered += 1,
555 MutantStatus::Pending => covered += 1,
556 MutantStatus::Killed
557 | MutantStatus::Timeout
558 | MutantStatus::Survived
559 | MutantStatus::RuntimeError
560 | MutantStatus::Flaky
561 | MutantStatus::CompileError
562 if !result.covering_tests.is_empty() || result.static_mutant =>
563 {
564 covered += 1;
565 }
566 _ => {}
567 }
568 }
569 Self {
570 counts,
571 score: counts.score(),
572 coverage: ratio(covered, covered + uncovered),
573 }
574 }
575}
576
577#[expect(
578 clippy::cast_precision_loss,
579 reason = "mutant counts are far below 2^52"
580)]
581fn ratio(part: usize, whole: usize) -> Option<f64> {
582 (whole > 0).then(|| part as f64 / whole as f64)
583}
584
585#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
588#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
589pub struct MutationBaselineFailure {
590 pub file: Option<String>,
593 #[serde(default, skip_serializing_if = "Option::is_none")]
595 pub test_id: Option<String>,
596 #[serde(default, skip_serializing_if = "Option::is_none")]
599 pub message: Option<String>,
600 #[serde(default, skip_serializing_if = "Vec::is_empty")]
604 pub mutant_ids: Vec<String>,
605}
606
607#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
609#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
610pub struct MutationSkippedFile {
611 pub path: String,
613 pub reason: String,
615}
616
617#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
620#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
621pub struct MutationWorktreeCheck {
622 pub checked: bool,
624 pub allow_dirty: bool,
626 #[serde(default, skip_serializing_if = "Vec::is_empty")]
628 pub dirty_at_start: Vec<String>,
629 pub changed_paths: Vec<String>,
631}
632
633#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
635#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
636pub struct MutationRunIssue {
637 pub code: String,
641 pub message: String,
643 #[serde(default, skip_serializing_if = "Option::is_none")]
645 pub mutant_id: Option<String>,
646 #[serde(default, skip_serializing_if = "Option::is_none")]
648 pub stderr_tail: Option<String>,
649}
650
651#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
653#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
654pub struct MutationTimings {
655 pub plan_ms: u64,
657 pub setup_ms: u64,
659 pub dry_run_ms: u64,
661 pub mutants_ms: u64,
663 pub retry_ms: u64,
665 pub typecheck_ms: u64,
668}
669
670#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
672#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
673#[serde(rename_all = "kebab-case")]
674pub enum MutationTypecheckSkipReason {
675 NoProject,
677 NotTypeChecked,
680 SourceChanged,
682 FileMissing,
684 UnsupportedFile,
687}
688
689#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
691#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
692pub struct MutationTypecheckSkip {
693 pub reason: MutationTypecheckSkipReason,
695 pub count: usize,
697}
698
699#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
701#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
702pub struct MutationTypecheckOutput {
703 pub completed: bool,
707 pub survivors: usize,
709 pub checked: usize,
711 pub compile_errors: usize,
714 #[serde(default, skip_serializing_if = "Vec::is_empty")]
716 pub skipped: Vec<MutationTypecheckSkip>,
717}
718
719#[derive(Debug, Clone, PartialEq, Serialize)]
721#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
722pub struct MutationReport {
723 pub schema_version: MutationSchemaVersion,
725 pub version: ToolVersion,
727 pub elapsed_ms: ElapsedMs,
729 pub completion: MutationCompletion,
731 pub scope: MutationScopeOutput,
733 #[serde(default, skip_serializing_if = "Option::is_none")]
736 pub runner: Option<MutationRunnerOutput>,
737 #[serde(default, skip_serializing_if = "Option::is_none")]
740 pub cache: Option<MutationCacheOutput>,
741 pub summary: MutationSummary,
743 pub mutants: Vec<MutantResult>,
745 pub functions: Vec<MutationFunctionResult>,
747 pub no_test_files: bool,
749 pub baseline_failures: Vec<MutationBaselineFailure>,
751 pub skipped_files: Vec<MutationSkippedFile>,
753 pub unmatched_paths: Vec<String>,
755 pub unloaded_paths: Vec<String>,
757 pub worktree: MutationWorktreeCheck,
759 pub errors: Vec<MutationRunIssue>,
761 pub timings: MutationTimings,
763 pub files: Vec<MutationPlanFile>,
767 #[serde(default, skip_serializing_if = "Option::is_none")]
770 pub verdicts: Option<MutationVerdictJoin>,
771 #[serde(default, skip_serializing_if = "Option::is_none")]
774 pub typecheck: Option<MutationTypecheckOutput>,
775}
776
777#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
779#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
780pub enum MutationVerdictSchemaVersion {
781 #[serde(rename = "1")]
783 V1,
784}
785
786#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
788#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
789#[serde(rename_all = "kebab-case")]
790pub enum MutationVerdictKind {
791 Equivalent,
794}
795
796#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
798#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
799#[serde(deny_unknown_fields)]
800pub struct MutationVerdict {
801 pub review_key: String,
806 pub verdict: MutationVerdictKind,
808 #[serde(default, skip_serializing_if = "Option::is_none")]
810 pub note: Option<String>,
811}
812
813#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
816#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
817#[serde(deny_unknown_fields)]
818pub struct MutationVerdictInput {
819 pub schema_version: MutationVerdictSchemaVersion,
821 pub verdicts: Vec<MutationVerdict>,
823}
824
825#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
827#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
828pub struct MutationVerdictJoin {
829 pub verdicts_sha256: String,
831 pub applied: Vec<String>,
833 pub contradicted: Vec<String>,
836 pub deferred: Vec<String>,
839 pub unmatched: Vec<String>,
843}
844
845#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
847#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
848#[serde(rename_all = "snake_case")]
849pub enum MutationPlanDisposition {
850 Planned,
852 Pending,
856 NoCoverage,
858 Pruned,
860}
861
862#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
864#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
865pub struct MutationPlanMutant {
866 #[serde(flatten)]
868 pub location: MutantLocation,
869 pub disposition: MutationPlanDisposition,
871 #[serde(default, skip_serializing_if = "Option::is_none")]
873 pub prune_reason: Option<String>,
874 #[serde(default, skip_serializing_if = "Option::is_none")]
877 pub test_reach: Option<MutantTestReach>,
878}
879
880#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
882#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
883pub struct MutationPlanFile {
884 pub path: String,
886 pub source_sha256: String,
888 pub static_test_files: Vec<String>,
890}
891
892#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
894#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
895pub struct MutationPlanCountsOutput {
896 pub total: usize,
898 pub planned: usize,
900 pub pending: usize,
902 pub no_coverage: usize,
904 pub pruned_dead_code: usize,
906 pub pruned_generated: usize,
908 pub pruned_side_effect_sink: usize,
910 pub pruned_framework_config: usize,
912}
913
914#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
916#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
917pub struct MutationPlanOutput {
918 pub schema_version: MutationPlanSchemaVersion,
920 pub version: ToolVersion,
922 pub elapsed_ms: ElapsedMs,
924 pub scope: MutationScopeOutput,
926 pub counts: MutationPlanCountsOutput,
928 pub mutants: Vec<MutationPlanMutant>,
930 pub files: Vec<MutationPlanFile>,
932 pub test_files: Vec<String>,
934 pub no_test_files: bool,
936 pub framework_config_checked: bool,
938 pub skipped_files: Vec<MutationSkippedFile>,
940 pub unmatched_paths: Vec<String>,
942}
943
944#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
946#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
947pub enum MutationInspectSchemaVersion {
948 #[serde(rename = "1")]
950 V1,
951}
952
953#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
955#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
956#[serde(rename_all = "snake_case")]
957pub enum MutationInspectStatus {
958 Fresh,
960 Stale,
963}
964
965#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
967#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
968#[serde(rename_all = "kebab-case")]
969pub enum MutationStaleReason {
970 FileMissing,
972 NoDigest,
974 SourceChanged,
976 MutantNotReproduced,
979}
980
981#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
983#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
984pub struct MutationInspectCode {
985 pub original: String,
987 pub replacement: String,
989 pub original_lines: String,
991 pub mutated_lines: String,
993}
994
995#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
997#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
998pub struct MutationInspectOutput {
999 pub schema_version: MutationInspectSchemaVersion,
1001 pub version: ToolVersion,
1003 pub status: MutationInspectStatus,
1005 #[serde(default, skip_serializing_if = "Option::is_none")]
1007 pub stale_reason: Option<MutationStaleReason>,
1008 #[serde(flatten)]
1010 pub location: MutantLocation,
1011 #[serde(default, skip_serializing_if = "Option::is_none")]
1013 pub report_status: Option<MutantStatus>,
1014 pub report_sha256: Option<String>,
1016 pub current_sha256: Option<String>,
1019 pub code: Option<MutationInspectCode>,
1021 pub covering_tests: Vec<String>,
1023 pub static_test_files: Vec<String>,
1025 pub killed_by: Option<String>,
1027}
1028
1029pub fn serialize_mutation_json_output(
1035 output: MutationReport,
1036) -> Result<serde_json::Value, serde_json::Error> {
1037 serialize_named_json_output(output, "mutation")
1038}
1039
1040pub fn serialize_mutation_plan_json_output(
1046 output: MutationPlanOutput,
1047) -> Result<serde_json::Value, serde_json::Error> {
1048 serialize_named_json_output(output, "mutation-plan")
1049}
1050
1051pub fn serialize_mutation_inspect_json_output(
1057 output: MutationInspectOutput,
1058) -> Result<serde_json::Value, serde_json::Error> {
1059 serialize_named_json_output(output, "mutation-inspect")
1060}
1061
1062#[cfg(test)]
1063mod tests {
1064 use super::*;
1065
1066 fn counts(statuses: &[MutantStatus]) -> MutationStatusCounts {
1067 let mut counts = MutationStatusCounts::default();
1068 for status in statuses {
1069 counts.add(*status);
1070 }
1071 counts
1072 }
1073
1074 #[test]
1075 fn the_score_counts_executed_mutants_and_coverage_counts_reached_mutants() {
1076 let counts = counts(&[
1077 MutantStatus::Killed,
1078 MutantStatus::Killed,
1079 MutantStatus::Timeout,
1080 MutantStatus::Survived,
1081 MutantStatus::NoCoverage,
1082 MutantStatus::NoCoverage,
1083 MutantStatus::RuntimeError,
1084 MutantStatus::Flaky,
1085 MutantStatus::Pruned,
1086 MutantStatus::Pending,
1087 ]);
1088 assert_eq!(counts.total, 10);
1089 assert_eq!(counts.killed, 2);
1090 assert_eq!(counts.timeout, 1);
1091 assert_eq!(counts.survived, 1);
1092 assert_eq!(counts.no_coverage, 2);
1093 assert_eq!(counts.runtime_error, 1);
1094 assert_eq!(counts.flaky, 1);
1095 assert_eq!(counts.pruned, 1);
1096 assert_eq!(counts.pending, 1);
1097 assert_eq!(counts.score(), Some(0.75));
1098 }
1099
1100 fn result(status: MutantStatus, covering_tests: &[&str]) -> MutantResult {
1101 MutantResult {
1102 location: MutantLocation {
1103 mutant_id: "m".to_owned(),
1104 file: "src/a.ts".to_owned(),
1105 line: 1,
1106 column: 1,
1107 span: (0, 1),
1108 operator: "arithmetic-operator".to_owned(),
1109 function: None,
1110 function_line: None,
1111 rank: None,
1112 change: None,
1113 },
1114 status,
1115 prune_reason: None,
1116 killed_by: None,
1117 covering_tests: covering_tests
1118 .iter()
1119 .map(|test| (*test).to_owned())
1120 .collect(),
1121 static_mutant: false,
1122 duration_ms: None,
1123 test_reach: None,
1124 retried: false,
1125 attempts: Vec::new(),
1126 cached: false,
1127 type_error_codes: Vec::new(),
1128 }
1129 }
1130
1131 #[test]
1132 fn coverage_counts_executed_mutants_with_a_test_and_pending_mutants_as_covered() {
1133 let summary = MutationSummary::from_results(&[
1134 result(MutantStatus::Killed, &["t1"]),
1135 result(MutantStatus::Survived, &["t1"]),
1136 result(MutantStatus::RuntimeError, &["t2"]),
1137 result(MutantStatus::Pending, &[]),
1138 result(MutantStatus::NoCoverage, &[]),
1139 result(MutantStatus::Equivalent, &["t1"]),
1140 result(MutantStatus::Pruned, &[]),
1141 ]);
1142 assert_eq!(summary.score, Some(0.5));
1143 assert_eq!(summary.coverage, Some(0.8));
1144 assert_eq!(
1145 MutationSummary::from_results(&[result(MutantStatus::Pruned, &[])]).coverage,
1146 None
1147 );
1148 }
1149
1150 #[test]
1151 fn a_capped_run_reports_the_coverage_of_the_full_run() {
1152 let full = MutationSummary::from_results(&[
1153 result(MutantStatus::Killed, &["t1"]),
1154 result(MutantStatus::Survived, &["t1"]),
1155 result(MutantStatus::NoCoverage, &[]),
1156 result(MutantStatus::NoCoverage, &[]),
1157 ]);
1158 let capped = MutationSummary::from_results(&[
1159 result(MutantStatus::Killed, &["t1"]),
1160 result(MutantStatus::Pending, &[]),
1161 result(MutantStatus::NoCoverage, &[]),
1162 result(MutantStatus::NoCoverage, &[]),
1163 ]);
1164 assert_eq!(full.coverage, Some(0.5));
1165 assert_eq!(capped.coverage, Some(0.5));
1166 assert_eq!(full.score, Some(0.5));
1167 assert_eq!(capped.score, Some(1.0));
1168 }
1169
1170 #[test]
1171 fn equivalent_mutants_leave_the_score() {
1172 let mut counts = counts(&[
1173 MutantStatus::Killed,
1174 MutantStatus::Survived,
1175 MutantStatus::Survived,
1176 ]);
1177 assert_eq!(counts.score(), Some(1.0 / 3.0));
1178 counts.remove(MutantStatus::Survived);
1179 counts.add(MutantStatus::Equivalent);
1180 assert_eq!(counts.total, 3);
1181 assert_eq!(counts.survived, 1);
1182 assert_eq!(counts.equivalent, 1);
1183 assert_eq!(counts.score(), Some(0.5));
1184 counts.remove(MutantStatus::Timeout);
1185 assert_eq!(counts.total, 3, "removing an absent status changes nothing");
1186 }
1187
1188 #[test]
1189 fn verdict_documents_reject_unknown_fields_and_verdicts() {
1190 let valid: MutationVerdictInput = serde_json::from_value(serde_json::json!({
1191 "schema_version": "1",
1192 "verdicts": [{ "review_key": "abc", "verdict": "equivalent", "note": "same result" }]
1193 }))
1194 .expect("valid document");
1195 assert_eq!(valid.verdicts[0].verdict, MutationVerdictKind::Equivalent);
1196 for invalid in [
1197 serde_json::json!({ "schema_version": "1", "verdicts": [{ "review_key": "a", "verdict": "killed" }] }),
1198 serde_json::json!({ "schema_version": "1", "verdicts": [{ "review_key": "a", "verdict": "equivalent", "extra": 1 }] }),
1199 serde_json::json!({ "schema_version": "2", "verdicts": [] }),
1200 serde_json::json!({ "verdicts": [] }),
1201 ] {
1202 assert!(
1203 serde_json::from_value::<MutationVerdictInput>(invalid.clone()).is_err(),
1204 "{invalid}"
1205 );
1206 }
1207 }
1208
1209 #[test]
1210 fn the_score_is_null_when_no_mutant_counts() {
1211 let excluded = counts(&[
1212 MutantStatus::RuntimeError,
1213 MutantStatus::Pruned,
1214 MutantStatus::Flaky,
1215 ]);
1216 assert_eq!(excluded.score(), None);
1217 assert_eq!(counts(&[MutantStatus::Survived]).score(), Some(0.0));
1218 assert_eq!(counts(&[MutantStatus::Timeout]).score(), Some(1.0));
1219 }
1220
1221 #[test]
1222 fn completion_is_partial_only_with_a_reason() {
1223 let complete = MutationCompletion::from_reasons(Vec::new());
1224 assert_eq!(complete.status, MutationCompletionStatus::Complete);
1225 let partial = MutationCompletion::from_reasons(vec![
1226 MutationIncompleteReason::TimeBudget,
1227 MutationIncompleteReason::RuntimeError,
1228 MutationIncompleteReason::TimeBudget,
1229 ]);
1230 assert_eq!(partial.status, MutationCompletionStatus::Partial);
1231 assert_eq!(
1232 partial.reasons,
1233 vec![
1234 MutationIncompleteReason::RuntimeError,
1235 MutationIncompleteReason::TimeBudget
1236 ]
1237 );
1238 }
1239
1240 #[test]
1241 fn a_mutant_result_serializes_flat_with_snake_case_status_and_no_source_text() {
1242 let result = MutantResult {
1243 location: MutantLocation {
1244 mutant_id: "m1".to_owned(),
1245 file: "src/math.ts".to_owned(),
1246 line: 2,
1247 column: 10,
1248 span: (40, 45),
1249 operator: "arithmetic".to_owned(),
1250 function: Some("add".to_owned()),
1251 function_line: Some(1),
1252 rank: Some(1),
1253 change: None,
1254 },
1255 status: MutantStatus::NoCoverage,
1256 prune_reason: None,
1257 killed_by: None,
1258 covering_tests: Vec::new(),
1259 static_mutant: false,
1260 duration_ms: None,
1261 test_reach: None,
1262 retried: false,
1263 attempts: Vec::new(),
1264 cached: false,
1265 type_error_codes: Vec::new(),
1266 };
1267 let value = serde_json::to_value(&result).expect("serializes");
1268 assert_eq!(
1269 value,
1270 serde_json::json!({
1271 "mutant_id": "m1",
1272 "file": "src/math.ts",
1273 "line": 2,
1274 "column": 10,
1275 "span": [40, 45],
1276 "operator": "arithmetic",
1277 "function": "add",
1278 "function_line": 1,
1279 "rank": 1,
1280 "status": "no_coverage"
1281 })
1282 );
1283 }
1284 #[test]
1285 fn a_retried_mutant_serializes_each_attempt_in_run_order() {
1286 let attempt = |status, duration_ms, failure_kind| MutantAttempt {
1287 status,
1288 duration_ms: Some(duration_ms),
1289 killed_by: None,
1290 failure_kind,
1291 detail: None,
1292 };
1293 let result = MutantResult {
1294 location: MutantLocation {
1295 mutant_id: "m1".to_owned(),
1296 file: "src/math.ts".to_owned(),
1297 line: 2,
1298 column: 10,
1299 span: (40, 45),
1300 operator: "arithmetic".to_owned(),
1301 function: None,
1302 function_line: None,
1303 rank: None,
1304 change: None,
1305 },
1306 status: MutantStatus::Flaky,
1307 prune_reason: None,
1308 killed_by: None,
1309 covering_tests: Vec::new(),
1310 static_mutant: false,
1311 duration_ms: Some(5000),
1312 test_reach: None,
1313 retried: true,
1314 attempts: vec![
1315 attempt(
1316 MutantStatus::Timeout,
1317 5000,
1318 Some(MutantFailureKind::Timeout),
1319 ),
1320 attempt(MutantStatus::Survived, 12, None),
1321 ],
1322 cached: false,
1323 type_error_codes: Vec::new(),
1324 };
1325 let value = serde_json::to_value(&result).expect("serializes");
1326 assert_eq!(
1327 value["attempts"],
1328 serde_json::json!([
1329 {"status": "timeout", "duration_ms": 5000, "failure_kind": "timeout"},
1330 {"status": "survived", "duration_ms": 12}
1331 ])
1332 );
1333 assert_eq!(value["retried"], serde_json::json!(true));
1334 }
1335}