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 = "std::ops::Not::not")]
403 pub resumed: bool,
404 #[serde(default, skip_serializing_if = "Vec::is_empty")]
407 pub type_error_codes: Vec<u32>,
408}
409
410#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
412#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
413pub struct MutantAttempt {
414 pub status: MutantStatus,
416 #[serde(default, skip_serializing_if = "Option::is_none")]
419 pub duration_ms: Option<u64>,
420 #[serde(default, skip_serializing_if = "Option::is_none")]
422 pub killed_by: Option<String>,
423 #[serde(default, skip_serializing_if = "Option::is_none")]
426 pub failure_kind: Option<MutantFailureKind>,
427 #[serde(default, skip_serializing_if = "Option::is_none")]
429 pub detail: Option<String>,
430}
431
432#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
434#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
435#[serde(rename_all = "snake_case")]
436pub enum MutantFailureKind {
437 Assertion,
439 Error,
441 Timeout,
443}
444
445impl MutantFailureKind {
446 #[must_use]
449 pub fn from_wire(value: &str) -> Option<Self> {
450 match value {
451 "assertion" => Some(Self::Assertion),
452 "error" => Some(Self::Error),
453 "timeout" => Some(Self::Timeout),
454 _ => None,
455 }
456 }
457}
458
459#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
468#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
469pub struct MutantTestReach {
470 pub reach: TestReachClass,
472 #[serde(default, skip_serializing_if = "Option::is_none")]
475 pub export_name: Option<String>,
476 #[serde(default, skip_serializing_if = "Vec::is_empty")]
479 pub tests: Vec<String>,
480 #[serde(default, skip_serializing_if = "Vec::is_empty")]
483 pub mocked_targets: Vec<String>,
484}
485
486#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
488#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
489pub struct MutationCacheOutput {
490 pub hits: usize,
492 pub stored: usize,
494 pub not_keyed: Vec<String>,
499}
500
501#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
503#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
504pub struct MutationResumeOutput {
505 pub journal: String,
511 pub reused: usize,
513 pub rejected: usize,
518 #[serde(default, skip_serializing_if = "Option::is_none")]
522 pub note: Option<String>,
523}
524
525#[derive(Debug, Clone, PartialEq, Serialize)]
527#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
528pub struct MutationFunctionResult {
529 pub file: String,
531 pub name: Option<String>,
534 pub line: Option<u32>,
536 pub end_line: Option<u32>,
538 pub in_diff: bool,
540 #[serde(default, skip_serializing_if = "Option::is_none")]
542 pub cyclomatic: Option<u16>,
543 pub counts: MutationStatusCounts,
545 pub score: Option<f64>,
547 #[serde(default, skip_serializing_if = "Vec::is_empty")]
549 pub survived: Vec<String>,
550}
551
552#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
554#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
555pub struct MutationSummary {
556 pub counts: MutationStatusCounts,
558 pub score: Option<f64>,
563 pub coverage: Option<f64>,
571}
572
573impl MutationSummary {
574 #[must_use]
576 pub fn from_results(results: &[MutantResult]) -> Self {
577 let mut counts = MutationStatusCounts::default();
578 let (mut covered, mut uncovered) = (0usize, 0usize);
579 for result in results {
580 counts.add(result.status);
581 match result.status {
582 MutantStatus::NoCoverage => uncovered += 1,
583 MutantStatus::Pending => covered += 1,
584 MutantStatus::Killed
585 | MutantStatus::Timeout
586 | MutantStatus::Survived
587 | MutantStatus::RuntimeError
588 | MutantStatus::Flaky
589 | MutantStatus::CompileError
590 if !result.covering_tests.is_empty() || result.static_mutant =>
591 {
592 covered += 1;
593 }
594 _ => {}
595 }
596 }
597 Self {
598 counts,
599 score: counts.score(),
600 coverage: ratio(covered, covered + uncovered),
601 }
602 }
603}
604
605#[expect(
606 clippy::cast_precision_loss,
607 reason = "mutant counts are far below 2^52"
608)]
609fn ratio(part: usize, whole: usize) -> Option<f64> {
610 (whole > 0).then(|| part as f64 / whole as f64)
611}
612
613#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
616#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
617pub struct MutationBaselineFailure {
618 pub file: Option<String>,
621 #[serde(default, skip_serializing_if = "Option::is_none")]
623 pub test_id: Option<String>,
624 #[serde(default, skip_serializing_if = "Option::is_none")]
627 pub message: Option<String>,
628 #[serde(default, skip_serializing_if = "Vec::is_empty")]
632 pub mutant_ids: Vec<String>,
633}
634
635#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
637#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
638pub struct MutationSkippedFile {
639 pub path: String,
641 pub reason: String,
643}
644
645#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
648#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
649pub struct MutationWorktreeCheck {
650 pub checked: bool,
652 pub allow_dirty: bool,
654 #[serde(default, skip_serializing_if = "Vec::is_empty")]
656 pub dirty_at_start: Vec<String>,
657 pub changed_paths: Vec<String>,
659}
660
661#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
663#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
664pub struct MutationRunIssue {
665 pub code: String,
670 pub message: String,
672 #[serde(default, skip_serializing_if = "Option::is_none")]
674 pub mutant_id: Option<String>,
675 #[serde(default, skip_serializing_if = "Option::is_none")]
677 pub stderr_tail: Option<String>,
678}
679
680#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
682#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
683pub struct MutationTimings {
684 pub plan_ms: u64,
686 pub setup_ms: u64,
688 pub dry_run_ms: u64,
690 pub mutants_ms: u64,
692 pub retry_ms: u64,
694 pub typecheck_ms: u64,
697}
698
699#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
701#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
702#[serde(rename_all = "kebab-case")]
703pub enum MutationTypecheckSkipReason {
704 NoProject,
706 NotTypeChecked,
709 SourceChanged,
711 FileMissing,
713 UnsupportedFile,
716}
717
718#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
720#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
721pub struct MutationTypecheckSkip {
722 pub reason: MutationTypecheckSkipReason,
724 pub count: usize,
726}
727
728#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
730#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
731pub struct MutationTypecheckOutput {
732 pub completed: bool,
736 pub survivors: usize,
738 pub checked: usize,
740 pub compile_errors: usize,
743 #[serde(default, skip_serializing_if = "Vec::is_empty")]
745 pub skipped: Vec<MutationTypecheckSkip>,
746}
747
748#[derive(Debug, Clone, PartialEq, Serialize)]
750#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
751pub struct MutationReport {
752 pub schema_version: MutationSchemaVersion,
754 pub version: ToolVersion,
756 pub elapsed_ms: ElapsedMs,
758 pub completion: MutationCompletion,
760 pub scope: MutationScopeOutput,
762 #[serde(default, skip_serializing_if = "Option::is_none")]
765 pub runner: Option<MutationRunnerOutput>,
766 #[serde(default, skip_serializing_if = "Option::is_none")]
769 pub cache: Option<MutationCacheOutput>,
770 #[serde(default, skip_serializing_if = "Option::is_none")]
772 pub resume: Option<MutationResumeOutput>,
773 pub summary: MutationSummary,
775 pub mutants: Vec<MutantResult>,
777 pub functions: Vec<MutationFunctionResult>,
779 pub no_test_files: bool,
781 pub baseline_failures: Vec<MutationBaselineFailure>,
783 pub skipped_files: Vec<MutationSkippedFile>,
785 pub unmatched_paths: Vec<String>,
787 pub unloaded_paths: Vec<String>,
789 pub worktree: MutationWorktreeCheck,
791 pub errors: Vec<MutationRunIssue>,
793 pub timings: MutationTimings,
795 pub files: Vec<MutationPlanFile>,
799 #[serde(default, skip_serializing_if = "Option::is_none")]
802 pub verdicts: Option<MutationVerdictJoin>,
803 #[serde(default, skip_serializing_if = "Option::is_none")]
806 pub typecheck: Option<MutationTypecheckOutput>,
807}
808
809#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
811#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
812pub enum MutationVerdictSchemaVersion {
813 #[serde(rename = "1")]
815 V1,
816}
817
818#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
820#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
821#[serde(rename_all = "kebab-case")]
822pub enum MutationVerdictKind {
823 Equivalent,
826}
827
828#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
830#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
831#[serde(deny_unknown_fields)]
832pub struct MutationVerdict {
833 pub review_key: String,
838 pub verdict: MutationVerdictKind,
840 #[serde(default, skip_serializing_if = "Option::is_none")]
842 pub note: Option<String>,
843}
844
845#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
848#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
849#[serde(deny_unknown_fields)]
850pub struct MutationVerdictInput {
851 pub schema_version: MutationVerdictSchemaVersion,
853 pub verdicts: Vec<MutationVerdict>,
855}
856
857#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
859#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
860pub struct MutationVerdictJoin {
861 pub verdicts_sha256: String,
863 pub applied: Vec<String>,
865 pub contradicted: Vec<String>,
868 pub deferred: Vec<String>,
871 pub unmatched: Vec<String>,
875}
876
877#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
879#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
880#[serde(rename_all = "snake_case")]
881pub enum MutationPlanDisposition {
882 Planned,
884 Pending,
888 NoCoverage,
890 Pruned,
892}
893
894#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
896#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
897pub struct MutationPlanMutant {
898 #[serde(flatten)]
900 pub location: MutantLocation,
901 pub disposition: MutationPlanDisposition,
903 #[serde(default, skip_serializing_if = "Option::is_none")]
905 pub prune_reason: Option<String>,
906 #[serde(default, skip_serializing_if = "Option::is_none")]
909 pub test_reach: Option<MutantTestReach>,
910}
911
912#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
914#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
915pub struct MutationPlanFile {
916 pub path: String,
918 pub source_sha256: String,
920 pub static_test_files: Vec<String>,
922}
923
924#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
926#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
927pub struct MutationPlanCountsOutput {
928 pub total: usize,
930 pub planned: usize,
932 pub pending: usize,
934 pub no_coverage: usize,
936 pub pruned_dead_code: usize,
938 pub pruned_generated: usize,
940 pub pruned_side_effect_sink: usize,
942 pub pruned_framework_config: usize,
944}
945
946#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
948#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
949pub struct MutationPlanOutput {
950 pub schema_version: MutationPlanSchemaVersion,
952 pub version: ToolVersion,
954 pub elapsed_ms: ElapsedMs,
956 pub scope: MutationScopeOutput,
958 pub counts: MutationPlanCountsOutput,
960 pub mutants: Vec<MutationPlanMutant>,
962 pub files: Vec<MutationPlanFile>,
964 pub test_files: Vec<String>,
966 pub no_test_files: bool,
968 pub framework_config_checked: bool,
970 pub skipped_files: Vec<MutationSkippedFile>,
972 pub unmatched_paths: Vec<String>,
974}
975
976#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
978#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
979pub enum MutationInspectSchemaVersion {
980 #[serde(rename = "1")]
982 V1,
983}
984
985#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
987#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
988#[serde(rename_all = "snake_case")]
989pub enum MutationInspectStatus {
990 Fresh,
992 Stale,
995}
996
997#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
999#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1000#[serde(rename_all = "kebab-case")]
1001pub enum MutationStaleReason {
1002 FileMissing,
1004 NoDigest,
1006 SourceChanged,
1008 MutantNotReproduced,
1011}
1012
1013#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1015#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1016pub struct MutationInspectCode {
1017 pub original: String,
1019 pub replacement: String,
1021 pub original_lines: String,
1023 pub mutated_lines: String,
1025}
1026
1027#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1029#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1030pub struct MutationInspectOutput {
1031 pub schema_version: MutationInspectSchemaVersion,
1033 pub version: ToolVersion,
1035 pub status: MutationInspectStatus,
1037 #[serde(default, skip_serializing_if = "Option::is_none")]
1039 pub stale_reason: Option<MutationStaleReason>,
1040 #[serde(flatten)]
1042 pub location: MutantLocation,
1043 #[serde(default, skip_serializing_if = "Option::is_none")]
1045 pub report_status: Option<MutantStatus>,
1046 pub report_sha256: Option<String>,
1048 pub current_sha256: Option<String>,
1051 pub code: Option<MutationInspectCode>,
1053 pub covering_tests: Vec<String>,
1055 pub static_test_files: Vec<String>,
1057 pub killed_by: Option<String>,
1059}
1060
1061pub fn serialize_mutation_json_output(
1067 output: MutationReport,
1068) -> Result<serde_json::Value, serde_json::Error> {
1069 serialize_named_json_output(output, "mutation")
1070}
1071
1072pub fn serialize_mutation_plan_json_output(
1078 output: MutationPlanOutput,
1079) -> Result<serde_json::Value, serde_json::Error> {
1080 serialize_named_json_output(output, "mutation-plan")
1081}
1082
1083pub fn serialize_mutation_inspect_json_output(
1089 output: MutationInspectOutput,
1090) -> Result<serde_json::Value, serde_json::Error> {
1091 serialize_named_json_output(output, "mutation-inspect")
1092}
1093
1094#[cfg(test)]
1095mod tests {
1096 use super::*;
1097
1098 fn counts(statuses: &[MutantStatus]) -> MutationStatusCounts {
1099 let mut counts = MutationStatusCounts::default();
1100 for status in statuses {
1101 counts.add(*status);
1102 }
1103 counts
1104 }
1105
1106 #[test]
1107 fn the_score_counts_executed_mutants_and_coverage_counts_reached_mutants() {
1108 let counts = counts(&[
1109 MutantStatus::Killed,
1110 MutantStatus::Killed,
1111 MutantStatus::Timeout,
1112 MutantStatus::Survived,
1113 MutantStatus::NoCoverage,
1114 MutantStatus::NoCoverage,
1115 MutantStatus::RuntimeError,
1116 MutantStatus::Flaky,
1117 MutantStatus::Pruned,
1118 MutantStatus::Pending,
1119 ]);
1120 assert_eq!(counts.total, 10);
1121 assert_eq!(counts.killed, 2);
1122 assert_eq!(counts.timeout, 1);
1123 assert_eq!(counts.survived, 1);
1124 assert_eq!(counts.no_coverage, 2);
1125 assert_eq!(counts.runtime_error, 1);
1126 assert_eq!(counts.flaky, 1);
1127 assert_eq!(counts.pruned, 1);
1128 assert_eq!(counts.pending, 1);
1129 assert_eq!(counts.score(), Some(0.75));
1130 }
1131
1132 fn result(status: MutantStatus, covering_tests: &[&str]) -> MutantResult {
1133 MutantResult {
1134 location: MutantLocation {
1135 mutant_id: "m".to_owned(),
1136 file: "src/a.ts".to_owned(),
1137 line: 1,
1138 column: 1,
1139 span: (0, 1),
1140 operator: "arithmetic-operator".to_owned(),
1141 function: None,
1142 function_line: None,
1143 rank: None,
1144 change: None,
1145 },
1146 status,
1147 prune_reason: None,
1148 killed_by: None,
1149 covering_tests: covering_tests
1150 .iter()
1151 .map(|test| (*test).to_owned())
1152 .collect(),
1153 static_mutant: false,
1154 duration_ms: None,
1155 test_reach: None,
1156 retried: false,
1157 attempts: Vec::new(),
1158 cached: false,
1159 resumed: false,
1160 type_error_codes: Vec::new(),
1161 }
1162 }
1163
1164 #[test]
1165 fn coverage_counts_executed_mutants_with_a_test_and_pending_mutants_as_covered() {
1166 let summary = MutationSummary::from_results(&[
1167 result(MutantStatus::Killed, &["t1"]),
1168 result(MutantStatus::Survived, &["t1"]),
1169 result(MutantStatus::RuntimeError, &["t2"]),
1170 result(MutantStatus::Pending, &[]),
1171 result(MutantStatus::NoCoverage, &[]),
1172 result(MutantStatus::Equivalent, &["t1"]),
1173 result(MutantStatus::Pruned, &[]),
1174 ]);
1175 assert_eq!(summary.score, Some(0.5));
1176 assert_eq!(summary.coverage, Some(0.8));
1177 assert_eq!(
1178 MutationSummary::from_results(&[result(MutantStatus::Pruned, &[])]).coverage,
1179 None
1180 );
1181 }
1182
1183 #[test]
1184 fn a_capped_run_reports_the_coverage_of_the_full_run() {
1185 let full = MutationSummary::from_results(&[
1186 result(MutantStatus::Killed, &["t1"]),
1187 result(MutantStatus::Survived, &["t1"]),
1188 result(MutantStatus::NoCoverage, &[]),
1189 result(MutantStatus::NoCoverage, &[]),
1190 ]);
1191 let capped = MutationSummary::from_results(&[
1192 result(MutantStatus::Killed, &["t1"]),
1193 result(MutantStatus::Pending, &[]),
1194 result(MutantStatus::NoCoverage, &[]),
1195 result(MutantStatus::NoCoverage, &[]),
1196 ]);
1197 assert_eq!(full.coverage, Some(0.5));
1198 assert_eq!(capped.coverage, Some(0.5));
1199 assert_eq!(full.score, Some(0.5));
1200 assert_eq!(capped.score, Some(1.0));
1201 }
1202
1203 #[test]
1204 fn equivalent_mutants_leave_the_score() {
1205 let mut counts = counts(&[
1206 MutantStatus::Killed,
1207 MutantStatus::Survived,
1208 MutantStatus::Survived,
1209 ]);
1210 assert_eq!(counts.score(), Some(1.0 / 3.0));
1211 counts.remove(MutantStatus::Survived);
1212 counts.add(MutantStatus::Equivalent);
1213 assert_eq!(counts.total, 3);
1214 assert_eq!(counts.survived, 1);
1215 assert_eq!(counts.equivalent, 1);
1216 assert_eq!(counts.score(), Some(0.5));
1217 counts.remove(MutantStatus::Timeout);
1218 assert_eq!(counts.total, 3, "removing an absent status changes nothing");
1219 }
1220
1221 #[test]
1222 fn verdict_documents_reject_unknown_fields_and_verdicts() {
1223 let valid: MutationVerdictInput = serde_json::from_value(serde_json::json!({
1224 "schema_version": "1",
1225 "verdicts": [{ "review_key": "abc", "verdict": "equivalent", "note": "same result" }]
1226 }))
1227 .expect("valid document");
1228 assert_eq!(valid.verdicts[0].verdict, MutationVerdictKind::Equivalent);
1229 for invalid in [
1230 serde_json::json!({ "schema_version": "1", "verdicts": [{ "review_key": "a", "verdict": "killed" }] }),
1231 serde_json::json!({ "schema_version": "1", "verdicts": [{ "review_key": "a", "verdict": "equivalent", "extra": 1 }] }),
1232 serde_json::json!({ "schema_version": "2", "verdicts": [] }),
1233 serde_json::json!({ "verdicts": [] }),
1234 ] {
1235 assert!(
1236 serde_json::from_value::<MutationVerdictInput>(invalid.clone()).is_err(),
1237 "{invalid}"
1238 );
1239 }
1240 }
1241
1242 #[test]
1243 fn the_score_is_null_when_no_mutant_counts() {
1244 let excluded = counts(&[
1245 MutantStatus::RuntimeError,
1246 MutantStatus::Pruned,
1247 MutantStatus::Flaky,
1248 ]);
1249 assert_eq!(excluded.score(), None);
1250 assert_eq!(counts(&[MutantStatus::Survived]).score(), Some(0.0));
1251 assert_eq!(counts(&[MutantStatus::Timeout]).score(), Some(1.0));
1252 }
1253
1254 #[test]
1255 fn completion_is_partial_only_with_a_reason() {
1256 let complete = MutationCompletion::from_reasons(Vec::new());
1257 assert_eq!(complete.status, MutationCompletionStatus::Complete);
1258 let partial = MutationCompletion::from_reasons(vec![
1259 MutationIncompleteReason::TimeBudget,
1260 MutationIncompleteReason::RuntimeError,
1261 MutationIncompleteReason::TimeBudget,
1262 ]);
1263 assert_eq!(partial.status, MutationCompletionStatus::Partial);
1264 assert_eq!(
1265 partial.reasons,
1266 vec![
1267 MutationIncompleteReason::RuntimeError,
1268 MutationIncompleteReason::TimeBudget
1269 ]
1270 );
1271 }
1272
1273 #[test]
1274 fn a_mutant_result_serializes_flat_with_snake_case_status_and_no_source_text() {
1275 let result = MutantResult {
1276 location: MutantLocation {
1277 mutant_id: "m1".to_owned(),
1278 file: "src/math.ts".to_owned(),
1279 line: 2,
1280 column: 10,
1281 span: (40, 45),
1282 operator: "arithmetic".to_owned(),
1283 function: Some("add".to_owned()),
1284 function_line: Some(1),
1285 rank: Some(1),
1286 change: None,
1287 },
1288 status: MutantStatus::NoCoverage,
1289 prune_reason: None,
1290 killed_by: None,
1291 covering_tests: Vec::new(),
1292 static_mutant: false,
1293 duration_ms: None,
1294 test_reach: None,
1295 retried: false,
1296 attempts: Vec::new(),
1297 cached: false,
1298 resumed: false,
1299 type_error_codes: Vec::new(),
1300 };
1301 let value = serde_json::to_value(&result).expect("serializes");
1302 assert_eq!(
1303 value,
1304 serde_json::json!({
1305 "mutant_id": "m1",
1306 "file": "src/math.ts",
1307 "line": 2,
1308 "column": 10,
1309 "span": [40, 45],
1310 "operator": "arithmetic",
1311 "function": "add",
1312 "function_line": 1,
1313 "rank": 1,
1314 "status": "no_coverage"
1315 })
1316 );
1317 }
1318 #[test]
1319 fn a_retried_mutant_serializes_each_attempt_in_run_order() {
1320 let attempt = |status, duration_ms, failure_kind| MutantAttempt {
1321 status,
1322 duration_ms: Some(duration_ms),
1323 killed_by: None,
1324 failure_kind,
1325 detail: None,
1326 };
1327 let result = MutantResult {
1328 location: MutantLocation {
1329 mutant_id: "m1".to_owned(),
1330 file: "src/math.ts".to_owned(),
1331 line: 2,
1332 column: 10,
1333 span: (40, 45),
1334 operator: "arithmetic".to_owned(),
1335 function: None,
1336 function_line: None,
1337 rank: None,
1338 change: None,
1339 },
1340 status: MutantStatus::Flaky,
1341 prune_reason: None,
1342 killed_by: None,
1343 covering_tests: Vec::new(),
1344 static_mutant: false,
1345 duration_ms: Some(5000),
1346 test_reach: None,
1347 retried: true,
1348 attempts: vec![
1349 attempt(
1350 MutantStatus::Timeout,
1351 5000,
1352 Some(MutantFailureKind::Timeout),
1353 ),
1354 attempt(MutantStatus::Survived, 12, None),
1355 ],
1356 cached: false,
1357 resumed: false,
1358 type_error_codes: Vec::new(),
1359 };
1360 let value = serde_json::to_value(&result).expect("serializes");
1361 assert_eq!(
1362 value["attempts"],
1363 serde_json::json!([
1364 {"status": "timeout", "duration_ms": 5000, "failure_kind": "timeout"},
1365 {"status": "survived", "duration_ms": 12}
1366 ])
1367 );
1368 assert_eq!(value["retried"], serde_json::json!(true));
1369 }
1370}