1use fallow_types::envelope::{ElapsedMs, ToolVersion};
8use serde::{Deserialize, Serialize};
9
10use crate::audit_test_reach::TestReachClass;
11use crate::root_envelopes::serialize_named_json_output;
12
13pub const MUTATION_SCHEMA_VERSION: u32 = 1;
15pub const MUTATION_PLAN_SCHEMA_VERSION: u32 = 1;
17pub const MUTATION_INSPECT_SCHEMA_VERSION: u32 = 1;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
22#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
23pub enum MutationSchemaVersion {
24 #[serde(rename = "1")]
26 V1,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
31#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
32pub enum MutationPlanSchemaVersion {
33 #[serde(rename = "1")]
35 V1,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
40#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
41#[serde(rename_all = "snake_case")]
42pub enum MutantStatus {
43 Killed,
45 Timeout,
49 Survived,
52 NoCoverage,
54 RuntimeError,
56 Flaky,
59 Pruned,
61 Pending,
63 Equivalent,
66 CompileError,
70}
71
72#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
74#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
75pub struct MutationStatusCounts {
76 pub total: usize,
78 pub killed: usize,
80 pub timeout: usize,
82 pub survived: usize,
84 pub no_coverage: usize,
86 pub runtime_error: usize,
88 pub flaky: usize,
91 pub pruned: usize,
93 pub pending: usize,
95 pub equivalent: usize,
97 #[serde(default)]
100 pub compile_error: usize,
101}
102
103impl MutationStatusCounts {
104 #[must_use]
107 pub const fn executed(&self) -> usize {
108 self.killed
109 + self.timeout
110 + self.survived
111 + self.runtime_error
112 + self.flaky
113 + self.compile_error
114 }
115
116 pub const fn add(&mut self, status: MutantStatus) {
118 self.total += 1;
119 match status {
120 MutantStatus::Killed => self.killed += 1,
121 MutantStatus::Timeout => self.timeout += 1,
122 MutantStatus::Survived => self.survived += 1,
123 MutantStatus::NoCoverage => self.no_coverage += 1,
124 MutantStatus::RuntimeError => self.runtime_error += 1,
125 MutantStatus::Flaky => self.flaky += 1,
126 MutantStatus::Pruned => self.pruned += 1,
127 MutantStatus::Pending => self.pending += 1,
128 MutantStatus::Equivalent => self.equivalent += 1,
129 MutantStatus::CompileError => self.compile_error += 1,
130 }
131 }
132
133 pub const fn remove(&mut self, status: MutantStatus) {
135 let count = match status {
136 MutantStatus::Killed => &mut self.killed,
137 MutantStatus::Timeout => &mut self.timeout,
138 MutantStatus::Survived => &mut self.survived,
139 MutantStatus::NoCoverage => &mut self.no_coverage,
140 MutantStatus::RuntimeError => &mut self.runtime_error,
141 MutantStatus::Flaky => &mut self.flaky,
142 MutantStatus::Pruned => &mut self.pruned,
143 MutantStatus::Pending => &mut self.pending,
144 MutantStatus::Equivalent => &mut self.equivalent,
145 MutantStatus::CompileError => &mut self.compile_error,
146 };
147 if *count > 0 {
148 *count -= 1;
149 self.total -= 1;
150 }
151 }
152
153 #[must_use]
161 #[expect(
162 clippy::cast_precision_loss,
163 reason = "mutant counts are far below 2^52"
164 )]
165 pub fn score(&self) -> Option<f64> {
166 let detected = self.killed + self.timeout;
167 let executed = detected + self.survived;
168 (executed > 0).then(|| detected as f64 / executed as f64)
169 }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
174#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
175#[serde(rename_all = "snake_case")]
176pub enum MutationCompletionStatus {
177 Complete,
180 Partial,
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
186#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
187#[serde(rename_all = "kebab-case")]
188pub enum MutationIncompleteReason {
189 RuntimeError,
191 MutantLimit,
193 TimeBudget,
195 SkippedFiles,
197 UnloadedFiles,
200 BaselineFailure,
202 WorktreeChanged,
204 TypecheckFailed,
207 WorktreeUnchecked,
209}
210
211#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
213#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
214pub struct MutationCompletion {
215 pub status: MutationCompletionStatus,
217 pub reasons: Vec<MutationIncompleteReason>,
219}
220
221impl MutationCompletion {
222 #[must_use]
225 pub fn from_reasons(mut reasons: Vec<MutationIncompleteReason>) -> Self {
226 reasons.sort_unstable();
227 reasons.dedup();
228 let status = if reasons.is_empty() {
229 MutationCompletionStatus::Complete
230 } else {
231 MutationCompletionStatus::Partial
232 };
233 Self { status, reasons }
234 }
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
239#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
240#[serde(rename_all = "snake_case")]
241pub enum MutationScopeKind {
242 Changed,
244 All,
246 Paths,
248}
249
250#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
252#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
253pub struct MutationScopeOutput {
254 pub kind: MutationScopeKind,
256 #[serde(default, skip_serializing_if = "Option::is_none")]
258 pub base_ref: Option<String>,
259}
260
261#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
263#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
264pub struct MutationRuntimeOutput {
265 pub name: String,
267 pub version: String,
269}
270
271#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
273#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
274pub struct MutationRunnerOutput {
275 pub name: String,
277 pub version: String,
279 pub protocol_version: u32,
281 pub runtime: MutationRuntimeOutput,
283 pub concurrency: usize,
285 #[serde(default, skip_serializing_if = "Vec::is_empty")]
290 pub notes: Vec<String>,
291}
292
293#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
295#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
296pub struct MutantLocation {
297 pub mutant_id: String,
299 pub file: String,
301 pub line: u32,
303 pub column: u32,
305 pub span: (u32, u32),
307 pub operator: String,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub function: Option<String>,
312 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub function_line: Option<u32>,
315 #[serde(default, skip_serializing_if = "Option::is_none")]
317 pub rank: Option<u32>,
318}
319
320#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
322#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
323pub struct MutantResult {
324 #[serde(flatten)]
326 pub location: MutantLocation,
327 pub status: MutantStatus,
329 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub prune_reason: Option<String>,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
340 pub killed_by: Option<String>,
341 #[serde(default, skip_serializing_if = "Vec::is_empty")]
343 pub covering_tests: Vec<String>,
344 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
346 pub static_mutant: bool,
347 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub duration_ms: Option<u64>,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
354 pub test_reach: Option<MutantTestReach>,
355 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
359 pub retried: bool,
360 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
363 pub cached: bool,
364 #[serde(default, skip_serializing_if = "Vec::is_empty")]
367 pub type_error_codes: Vec<u32>,
368}
369
370#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
379#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
380pub struct MutantTestReach {
381 pub reach: TestReachClass,
383 #[serde(default, skip_serializing_if = "Option::is_none")]
386 pub export_name: Option<String>,
387 #[serde(default, skip_serializing_if = "Vec::is_empty")]
390 pub tests: Vec<String>,
391 #[serde(default, skip_serializing_if = "Vec::is_empty")]
394 pub mocked_targets: Vec<String>,
395}
396
397#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
399#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
400pub struct MutationCacheOutput {
401 pub hits: usize,
403 pub stored: usize,
405 pub not_keyed: Vec<String>,
410}
411
412#[derive(Debug, Clone, PartialEq, Serialize)]
414#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
415pub struct MutationFunctionResult {
416 pub file: String,
418 pub name: Option<String>,
421 pub line: Option<u32>,
423 pub end_line: Option<u32>,
425 pub in_diff: bool,
427 #[serde(default, skip_serializing_if = "Option::is_none")]
429 pub cyclomatic: Option<u16>,
430 pub counts: MutationStatusCounts,
432 pub score: Option<f64>,
434 #[serde(default, skip_serializing_if = "Vec::is_empty")]
436 pub survived: Vec<String>,
437}
438
439#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
441#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
442pub struct MutationSummary {
443 pub counts: MutationStatusCounts,
445 pub score: Option<f64>,
450 pub coverage: Option<f64>,
458}
459
460impl MutationSummary {
461 #[must_use]
463 pub fn from_results(results: &[MutantResult]) -> Self {
464 let mut counts = MutationStatusCounts::default();
465 let (mut covered, mut uncovered) = (0usize, 0usize);
466 for result in results {
467 counts.add(result.status);
468 match result.status {
469 MutantStatus::NoCoverage => uncovered += 1,
470 MutantStatus::Pending => covered += 1,
471 MutantStatus::Killed
472 | MutantStatus::Timeout
473 | MutantStatus::Survived
474 | MutantStatus::RuntimeError
475 | MutantStatus::Flaky
476 | MutantStatus::CompileError
477 if !result.covering_tests.is_empty() || result.static_mutant =>
478 {
479 covered += 1;
480 }
481 _ => {}
482 }
483 }
484 Self {
485 counts,
486 score: counts.score(),
487 coverage: ratio(covered, covered + uncovered),
488 }
489 }
490}
491
492#[expect(
493 clippy::cast_precision_loss,
494 reason = "mutant counts are far below 2^52"
495)]
496fn ratio(part: usize, whole: usize) -> Option<f64> {
497 (whole > 0).then(|| part as f64 / whole as f64)
498}
499
500#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
503#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
504pub struct MutationBaselineFailure {
505 pub file: Option<String>,
508 #[serde(default, skip_serializing_if = "Option::is_none")]
510 pub test_id: Option<String>,
511 #[serde(default, skip_serializing_if = "Option::is_none")]
514 pub message: Option<String>,
515 #[serde(default, skip_serializing_if = "Vec::is_empty")]
519 pub mutant_ids: Vec<String>,
520}
521
522#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
524#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
525pub struct MutationSkippedFile {
526 pub path: String,
528 pub reason: String,
530}
531
532#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
535#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
536pub struct MutationWorktreeCheck {
537 pub checked: bool,
539 pub allow_dirty: bool,
541 #[serde(default, skip_serializing_if = "Vec::is_empty")]
543 pub dirty_at_start: Vec<String>,
544 pub changed_paths: Vec<String>,
546}
547
548#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
550#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
551pub struct MutationRunIssue {
552 pub code: String,
556 pub message: String,
558 #[serde(default, skip_serializing_if = "Option::is_none")]
560 pub mutant_id: Option<String>,
561 #[serde(default, skip_serializing_if = "Option::is_none")]
563 pub stderr_tail: Option<String>,
564}
565
566#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
568#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
569pub struct MutationTimings {
570 pub plan_ms: u64,
572 pub setup_ms: u64,
574 pub dry_run_ms: u64,
576 pub mutants_ms: u64,
578 pub retry_ms: u64,
580 pub typecheck_ms: u64,
583}
584
585#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
587#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
588#[serde(rename_all = "kebab-case")]
589pub enum MutationTypecheckSkipReason {
590 NoProject,
592 NotTypeChecked,
595 SourceChanged,
597 FileMissing,
599 UnsupportedFile,
602}
603
604#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
606#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
607pub struct MutationTypecheckSkip {
608 pub reason: MutationTypecheckSkipReason,
610 pub count: usize,
612}
613
614#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
616#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
617pub struct MutationTypecheckOutput {
618 pub completed: bool,
622 pub survivors: usize,
624 pub checked: usize,
626 pub compile_errors: usize,
629 #[serde(default, skip_serializing_if = "Vec::is_empty")]
631 pub skipped: Vec<MutationTypecheckSkip>,
632}
633
634#[derive(Debug, Clone, PartialEq, Serialize)]
636#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
637pub struct MutationReport {
638 pub schema_version: MutationSchemaVersion,
640 pub version: ToolVersion,
642 pub elapsed_ms: ElapsedMs,
644 pub completion: MutationCompletion,
646 pub scope: MutationScopeOutput,
648 #[serde(default, skip_serializing_if = "Option::is_none")]
651 pub runner: Option<MutationRunnerOutput>,
652 #[serde(default, skip_serializing_if = "Option::is_none")]
655 pub cache: Option<MutationCacheOutput>,
656 pub summary: MutationSummary,
658 pub mutants: Vec<MutantResult>,
660 pub functions: Vec<MutationFunctionResult>,
662 pub no_test_files: bool,
664 pub baseline_failures: Vec<MutationBaselineFailure>,
666 pub skipped_files: Vec<MutationSkippedFile>,
668 pub unmatched_paths: Vec<String>,
670 pub unloaded_paths: Vec<String>,
672 pub worktree: MutationWorktreeCheck,
674 pub errors: Vec<MutationRunIssue>,
676 pub timings: MutationTimings,
678 pub files: Vec<MutationPlanFile>,
682 #[serde(default, skip_serializing_if = "Option::is_none")]
685 pub verdicts: Option<MutationVerdictJoin>,
686 #[serde(default, skip_serializing_if = "Option::is_none")]
689 pub typecheck: Option<MutationTypecheckOutput>,
690}
691
692#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
694#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
695pub enum MutationVerdictSchemaVersion {
696 #[serde(rename = "1")]
698 V1,
699}
700
701#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
703#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
704#[serde(rename_all = "kebab-case")]
705pub enum MutationVerdictKind {
706 Equivalent,
709}
710
711#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
713#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
714#[serde(deny_unknown_fields)]
715pub struct MutationVerdict {
716 pub review_key: String,
721 pub verdict: MutationVerdictKind,
723 #[serde(default, skip_serializing_if = "Option::is_none")]
725 pub note: Option<String>,
726}
727
728#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
731#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
732#[serde(deny_unknown_fields)]
733pub struct MutationVerdictInput {
734 pub schema_version: MutationVerdictSchemaVersion,
736 pub verdicts: Vec<MutationVerdict>,
738}
739
740#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
742#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
743pub struct MutationVerdictJoin {
744 pub verdicts_sha256: String,
746 pub applied: Vec<String>,
748 pub contradicted: Vec<String>,
751 pub deferred: Vec<String>,
754 pub unmatched: Vec<String>,
758}
759
760#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
762#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
763#[serde(rename_all = "snake_case")]
764pub enum MutationPlanDisposition {
765 Planned,
767 Pending,
771 NoCoverage,
773 Pruned,
775}
776
777#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
779#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
780pub struct MutationPlanMutant {
781 #[serde(flatten)]
783 pub location: MutantLocation,
784 pub disposition: MutationPlanDisposition,
786 #[serde(default, skip_serializing_if = "Option::is_none")]
788 pub prune_reason: Option<String>,
789 #[serde(default, skip_serializing_if = "Option::is_none")]
792 pub test_reach: Option<MutantTestReach>,
793}
794
795#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
797#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
798pub struct MutationPlanFile {
799 pub path: String,
801 pub source_sha256: String,
803 pub static_test_files: Vec<String>,
805}
806
807#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
809#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
810pub struct MutationPlanCountsOutput {
811 pub total: usize,
813 pub planned: usize,
815 pub pending: usize,
817 pub no_coverage: usize,
819 pub pruned_dead_code: usize,
821 pub pruned_generated: usize,
823 pub pruned_side_effect_sink: usize,
825 pub pruned_framework_config: usize,
827}
828
829#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
831#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
832pub struct MutationPlanOutput {
833 pub schema_version: MutationPlanSchemaVersion,
835 pub version: ToolVersion,
837 pub elapsed_ms: ElapsedMs,
839 pub scope: MutationScopeOutput,
841 pub counts: MutationPlanCountsOutput,
843 pub mutants: Vec<MutationPlanMutant>,
845 pub files: Vec<MutationPlanFile>,
847 pub test_files: Vec<String>,
849 pub no_test_files: bool,
851 pub framework_config_checked: bool,
853 pub skipped_files: Vec<MutationSkippedFile>,
855 pub unmatched_paths: Vec<String>,
857}
858
859#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
861#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
862pub enum MutationInspectSchemaVersion {
863 #[serde(rename = "1")]
865 V1,
866}
867
868#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
870#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
871#[serde(rename_all = "snake_case")]
872pub enum MutationInspectStatus {
873 Fresh,
875 Stale,
878}
879
880#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
882#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
883#[serde(rename_all = "kebab-case")]
884pub enum MutationStaleReason {
885 FileMissing,
887 NoDigest,
889 SourceChanged,
891 MutantNotReproduced,
894}
895
896#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
898#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
899pub struct MutationInspectCode {
900 pub original: String,
902 pub replacement: String,
904 pub original_lines: String,
906 pub mutated_lines: String,
908}
909
910#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
912#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
913pub struct MutationInspectOutput {
914 pub schema_version: MutationInspectSchemaVersion,
916 pub version: ToolVersion,
918 pub status: MutationInspectStatus,
920 #[serde(default, skip_serializing_if = "Option::is_none")]
922 pub stale_reason: Option<MutationStaleReason>,
923 #[serde(flatten)]
925 pub location: MutantLocation,
926 #[serde(default, skip_serializing_if = "Option::is_none")]
928 pub report_status: Option<MutantStatus>,
929 pub report_sha256: Option<String>,
931 pub current_sha256: Option<String>,
934 pub code: Option<MutationInspectCode>,
936 pub covering_tests: Vec<String>,
938 pub static_test_files: Vec<String>,
940 pub killed_by: Option<String>,
942}
943
944pub fn serialize_mutation_json_output(
950 output: MutationReport,
951) -> Result<serde_json::Value, serde_json::Error> {
952 serialize_named_json_output(output, "mutation")
953}
954
955pub fn serialize_mutation_plan_json_output(
961 output: MutationPlanOutput,
962) -> Result<serde_json::Value, serde_json::Error> {
963 serialize_named_json_output(output, "mutation-plan")
964}
965
966pub fn serialize_mutation_inspect_json_output(
972 output: MutationInspectOutput,
973) -> Result<serde_json::Value, serde_json::Error> {
974 serialize_named_json_output(output, "mutation-inspect")
975}
976
977#[cfg(test)]
978mod tests {
979 use super::*;
980
981 fn counts(statuses: &[MutantStatus]) -> MutationStatusCounts {
982 let mut counts = MutationStatusCounts::default();
983 for status in statuses {
984 counts.add(*status);
985 }
986 counts
987 }
988
989 #[test]
990 fn the_score_counts_executed_mutants_and_coverage_counts_reached_mutants() {
991 let counts = counts(&[
992 MutantStatus::Killed,
993 MutantStatus::Killed,
994 MutantStatus::Timeout,
995 MutantStatus::Survived,
996 MutantStatus::NoCoverage,
997 MutantStatus::NoCoverage,
998 MutantStatus::RuntimeError,
999 MutantStatus::Flaky,
1000 MutantStatus::Pruned,
1001 MutantStatus::Pending,
1002 ]);
1003 assert_eq!(counts.total, 10);
1004 assert_eq!(counts.killed, 2);
1005 assert_eq!(counts.timeout, 1);
1006 assert_eq!(counts.survived, 1);
1007 assert_eq!(counts.no_coverage, 2);
1008 assert_eq!(counts.runtime_error, 1);
1009 assert_eq!(counts.flaky, 1);
1010 assert_eq!(counts.pruned, 1);
1011 assert_eq!(counts.pending, 1);
1012 assert_eq!(counts.score(), Some(0.75));
1013 }
1014
1015 fn result(status: MutantStatus, covering_tests: &[&str]) -> MutantResult {
1016 MutantResult {
1017 location: MutantLocation {
1018 mutant_id: "m".to_owned(),
1019 file: "src/a.ts".to_owned(),
1020 line: 1,
1021 column: 1,
1022 span: (0, 1),
1023 operator: "arithmetic-operator".to_owned(),
1024 function: None,
1025 function_line: None,
1026 rank: None,
1027 },
1028 status,
1029 prune_reason: None,
1030 killed_by: None,
1031 covering_tests: covering_tests
1032 .iter()
1033 .map(|test| (*test).to_owned())
1034 .collect(),
1035 static_mutant: false,
1036 duration_ms: None,
1037 test_reach: None,
1038 retried: false,
1039 cached: false,
1040 type_error_codes: Vec::new(),
1041 }
1042 }
1043
1044 #[test]
1045 fn coverage_counts_executed_mutants_with_a_test_and_pending_mutants_as_covered() {
1046 let summary = MutationSummary::from_results(&[
1047 result(MutantStatus::Killed, &["t1"]),
1048 result(MutantStatus::Survived, &["t1"]),
1049 result(MutantStatus::RuntimeError, &["t2"]),
1050 result(MutantStatus::Pending, &[]),
1051 result(MutantStatus::NoCoverage, &[]),
1052 result(MutantStatus::Equivalent, &["t1"]),
1053 result(MutantStatus::Pruned, &[]),
1054 ]);
1055 assert_eq!(summary.score, Some(0.5));
1056 assert_eq!(summary.coverage, Some(0.8));
1057 assert_eq!(
1058 MutationSummary::from_results(&[result(MutantStatus::Pruned, &[])]).coverage,
1059 None
1060 );
1061 }
1062
1063 #[test]
1064 fn a_capped_run_reports_the_coverage_of_the_full_run() {
1065 let full = MutationSummary::from_results(&[
1066 result(MutantStatus::Killed, &["t1"]),
1067 result(MutantStatus::Survived, &["t1"]),
1068 result(MutantStatus::NoCoverage, &[]),
1069 result(MutantStatus::NoCoverage, &[]),
1070 ]);
1071 let capped = MutationSummary::from_results(&[
1072 result(MutantStatus::Killed, &["t1"]),
1073 result(MutantStatus::Pending, &[]),
1074 result(MutantStatus::NoCoverage, &[]),
1075 result(MutantStatus::NoCoverage, &[]),
1076 ]);
1077 assert_eq!(full.coverage, Some(0.5));
1078 assert_eq!(capped.coverage, Some(0.5));
1079 assert_eq!(full.score, Some(0.5));
1080 assert_eq!(capped.score, Some(1.0));
1081 }
1082
1083 #[test]
1084 fn equivalent_mutants_leave_the_score() {
1085 let mut counts = counts(&[
1086 MutantStatus::Killed,
1087 MutantStatus::Survived,
1088 MutantStatus::Survived,
1089 ]);
1090 assert_eq!(counts.score(), Some(1.0 / 3.0));
1091 counts.remove(MutantStatus::Survived);
1092 counts.add(MutantStatus::Equivalent);
1093 assert_eq!(counts.total, 3);
1094 assert_eq!(counts.survived, 1);
1095 assert_eq!(counts.equivalent, 1);
1096 assert_eq!(counts.score(), Some(0.5));
1097 counts.remove(MutantStatus::Timeout);
1098 assert_eq!(counts.total, 3, "removing an absent status changes nothing");
1099 }
1100
1101 #[test]
1102 fn verdict_documents_reject_unknown_fields_and_verdicts() {
1103 let valid: MutationVerdictInput = serde_json::from_value(serde_json::json!({
1104 "schema_version": "1",
1105 "verdicts": [{ "review_key": "abc", "verdict": "equivalent", "note": "same result" }]
1106 }))
1107 .expect("valid document");
1108 assert_eq!(valid.verdicts[0].verdict, MutationVerdictKind::Equivalent);
1109 for invalid in [
1110 serde_json::json!({ "schema_version": "1", "verdicts": [{ "review_key": "a", "verdict": "killed" }] }),
1111 serde_json::json!({ "schema_version": "1", "verdicts": [{ "review_key": "a", "verdict": "equivalent", "extra": 1 }] }),
1112 serde_json::json!({ "schema_version": "2", "verdicts": [] }),
1113 serde_json::json!({ "verdicts": [] }),
1114 ] {
1115 assert!(
1116 serde_json::from_value::<MutationVerdictInput>(invalid.clone()).is_err(),
1117 "{invalid}"
1118 );
1119 }
1120 }
1121
1122 #[test]
1123 fn the_score_is_null_when_no_mutant_counts() {
1124 let excluded = counts(&[
1125 MutantStatus::RuntimeError,
1126 MutantStatus::Pruned,
1127 MutantStatus::Flaky,
1128 ]);
1129 assert_eq!(excluded.score(), None);
1130 assert_eq!(counts(&[MutantStatus::Survived]).score(), Some(0.0));
1131 assert_eq!(counts(&[MutantStatus::Timeout]).score(), Some(1.0));
1132 }
1133
1134 #[test]
1135 fn completion_is_partial_only_with_a_reason() {
1136 let complete = MutationCompletion::from_reasons(Vec::new());
1137 assert_eq!(complete.status, MutationCompletionStatus::Complete);
1138 let partial = MutationCompletion::from_reasons(vec![
1139 MutationIncompleteReason::TimeBudget,
1140 MutationIncompleteReason::RuntimeError,
1141 MutationIncompleteReason::TimeBudget,
1142 ]);
1143 assert_eq!(partial.status, MutationCompletionStatus::Partial);
1144 assert_eq!(
1145 partial.reasons,
1146 vec![
1147 MutationIncompleteReason::RuntimeError,
1148 MutationIncompleteReason::TimeBudget
1149 ]
1150 );
1151 }
1152
1153 #[test]
1154 fn a_mutant_result_serializes_flat_with_snake_case_status_and_no_source_text() {
1155 let result = MutantResult {
1156 location: MutantLocation {
1157 mutant_id: "m1".to_owned(),
1158 file: "src/math.ts".to_owned(),
1159 line: 2,
1160 column: 10,
1161 span: (40, 45),
1162 operator: "arithmetic".to_owned(),
1163 function: Some("add".to_owned()),
1164 function_line: Some(1),
1165 rank: Some(1),
1166 },
1167 status: MutantStatus::NoCoverage,
1168 prune_reason: None,
1169 killed_by: None,
1170 covering_tests: Vec::new(),
1171 static_mutant: false,
1172 duration_ms: None,
1173 test_reach: None,
1174 retried: false,
1175 cached: false,
1176 type_error_codes: Vec::new(),
1177 };
1178 let value = serde_json::to_value(&result).expect("serializes");
1179 assert_eq!(
1180 value,
1181 serde_json::json!({
1182 "mutant_id": "m1",
1183 "file": "src/math.ts",
1184 "line": 2,
1185 "column": 10,
1186 "span": [40, 45],
1187 "operator": "arithmetic",
1188 "function": "add",
1189 "function_line": 1,
1190 "rank": 1,
1191 "status": "no_coverage"
1192 })
1193 );
1194 }
1195}