diff --git a/epiphany-pipeline/src/lib.rs b/epiphany-pipeline/src/lib.rs index 4b5fdfbf..b8b0d79e 100644 --- a/epiphany-pipeline/src/lib.rs +++ b/epiphany-pipeline/src/lib.rs @@ -872,8 +872,12 @@ mod tests { commits: vec![ReportCommit { sha: sha(), subject: "Add the pipeline documents".into(), builds: true }], range: range(), verification: vec![evidence()], mutations: vec![MutationRecord { - label: l("M1"), rule: "key derivation".into(), location: location(), - before: "parent_cut(field, spec, 'r')?".into(), after: "spec".into(), commit: sha(), + // `.into()` on both, not `l()` and `sha()`: the type-level + // mutations (M19, M20) widen these fields to `Short`, and + // a sample spelled with the narrow constructors would stop + // compiling instead of letting the forgery through. + label: "M1".into(), rule: "key derivation".into(), location: location(), + before: "parent_cut(field, spec, 'r')?".into(), after: "spec".into(), commit: "5f98228d".into(), failed_as_expected: true, }], deviations: vec![Deviation { what: "names".into(), why: "glob exports".into() }], @@ -1671,8 +1675,11 @@ mod tests { fn mutation_records_carry_a_dot_free_label_and_a_commit() { let recorded = |label: &str, commit: &str| { let mut report = report_sample(); - report.mutations[0].label = Label(label.into()); - report.mutations[0].commit = Sha(commit.into()); + // Through `From<&str>`, which every text type has, so the + // widened types under M19 and M20 still compile and the + // forgeries reach validation. + report.mutations[0].label = label.into(); + report.mutations[0].commit = commit.into(); PipelineDocument::CutReport(report).validate() }; assert_eq!(recorded("M1", "5f98228d"), Ok(())); diff --git a/tools/eureka-cut6c-mutations.psd1 b/tools/eureka-cut6c-mutations.psd1 new file mode 100644 index 00000000..bc286fe1 --- /dev/null +++ b/tools/eureka-cut6c-mutations.psd1 @@ -0,0 +1,89 @@ +# Eureka Cut 6c mutation suite: the Ghostlight shapes. Entries only; the +# harness is tools/eureka-mutations.ps1. +# +# powershell -File tools/eureka-mutations.ps1 ` +# -Entries tools/eureka-cut6c-mutations.psd1 ` +# -Target epiphany-pipeline/src/lib.rs ` +# -Test 'cargo test -p epiphany-pipeline --lib' +# +# A retyping is the mutation-hostile case: the compiler catches the type +# change, so the tempting test constructs a valid value and asserts `Ok(())`, +# which stays green under every mutation below. Where the cut's content is a +# type, the mutation is a type-level mutation (M19, M20, M22) and its killer +# is a forgery the wider type admits. The key suite is blind to all of these +# because a resolution's key is outcome-invariant. +@{ + Mutations = @( + @{ + Id = 'M16' + Rule = 'A Superseded list is validated item by item, not only bounded: a wrong-kind referent inside it is refused.' + Test = 'tests::resolution_outcome_referents_are_parsed_ids_of_their_kind' + Old = ' Self::Superseded { by } => list(&format!("{field}.by"), by, 8),' + New = ' Self::Superseded { by } => bound(&format!("{field}.by"), 8, by.len()),' + }, + @{ + Id = 'M17' + Rule = 'Every resolution referent is validated as a full id of the kind it declares.' + Test = 'tests::resolution_outcome_referents_are_parsed_ids_of_their_kind' + Old = @' + match self { + Self::Superseded { by } => list(&format!("{field}.by"), by, 8), + Self::Answered { by } => by.validate(&format!("{field}.by")), + Self::Fixed { commit, by } => { + commit.validate(&format!("{field}.commit"))?; + by.validate(&format!("{field}.by")) + } + Self::Deferred { to } => to.validate(&format!("{field}.to")), + Self::Recorded { reason } | Self::Withdrawn { reason } => reason.validate(&format!("{field}.reason")), + } +'@ + New = @' + let _ = field; + Ok(()) +'@ + }, + @{ + Id = 'M18' + Rule = 'A fix''s commit is validated as a sha (ruling B): uppercase hex of legal length is refused.' + Test = 'tests::fixed_resolution_requires_a_commit_sha' + Old = ' commit.validate(&format!("{field}.commit"))?;' + New = ' let _ = commit;' + }, + @{ + Id = 'M19' + Rule = 'A mutation record''s label is a Label, so it is key-safe: a dotted label is refused.' + Test = 'tests::mutation_records_carry_a_dot_free_label_and_a_commit' + Old = 'pub struct MutationRecord { label: Label,' + New = 'pub struct MutationRecord { label: Short,' + }, + @{ + Id = 'M20' + Rule = 'A mutation record is pinned to a tree: its commit is a Sha, not free text.' + Test = 'tests::mutation_records_carry_a_dot_free_label_and_a_commit' + Old = 'commit: Sha, failed_as_expected: bool }' + New = 'commit: Short, failed_as_expected: bool }' + }, + # The two-line anchor carries the opener: the numbers alone also match + # the report sample's StructuralDelta. + @{ + Id = 'M21' + Rule = 'The estimate''s two u32 fields are not interchangeable: the sample spec estimates a net addition.' + Test = 'tests::the_sample_cut_spec_estimates_a_net_addition' + Old = @' + estimate: StructuralDelta { + lines_added: 900, lines_removed: 0, +'@ + New = @' + estimate: StructuralDelta { + lines_added: 0, lines_removed: 900, +'@ + }, + @{ + Id = 'M22' + Rule = 'A verdict claim names at most eight mutations.' + Test = 'tests::a_verdict_claim_names_the_promise_and_the_mutation_it_measured' + Old = 'mutations: Vec