use lgwks_ast::{Language, ParseError};
use lgwks_std::hash;
use lgwks_std::json::{Deserialize, Serialize};
pub const INSPECTION_VERSION: u32 = 1;
pub const ASSURANCE: &str = "structural rule set only: a clean result means no configured \
structural rule matched within a complete inspected scope, and is not proof the subject is \
free of logical bugs, free of vulnerabilities, or safe to execute";
pub const MAX_FINDINGS: usize = 1_024;
pub const MAX_PREVIEW_BYTES: usize = 200;
pub const MAX_OUTPUT_BYTES: usize = 256 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(crate = "lgwks_std::json::serde", rename_all = "snake_case")]
#[non_exhaustive]
pub enum Scope {
Structural,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct RuleSet {
identity: &'static str,
revision: u32,
}
impl RuleSet {
pub const STRUCTURAL_V1: RuleSet = RuleSet {
identity: "lgwks/structural",
revision: 1,
};
#[must_use]
pub const fn new(identity: &'static str, revision: u32) -> Self {
Self { identity, revision }
}
#[must_use]
pub const fn identity(self) -> &'static str {
self.identity
}
#[must_use]
pub const fn revision(self) -> u32 {
self.revision
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Budgets {
pub max_source_bytes: usize,
pub max_nodes: usize,
pub max_depth: usize,
pub max_work: usize,
pub max_findings: usize,
pub max_output_bytes: usize,
}
impl Budgets {
#[must_use]
pub const fn default_budgets() -> Self {
Self {
max_source_bytes: lgwks_ast::MAX_SOURCE_BYTES,
max_nodes: lgwks_ast::MAX_AST_NODES,
max_depth: 4_096,
max_work: lgwks_ast::MAX_AST_NODES.saturating_mul(RULES.len()),
max_findings: MAX_FINDINGS,
max_output_bytes: MAX_OUTPUT_BYTES,
}
}
#[must_use]
pub const fn new() -> Self {
Self::default_budgets()
}
#[must_use]
pub const fn with_source_bytes(mut self, bytes: usize) -> Self {
self.max_source_bytes = bytes;
self
}
#[must_use]
pub const fn with_nodes(mut self, nodes: usize) -> Self {
self.max_nodes = nodes;
self
}
#[must_use]
pub const fn with_depth(mut self, depth: usize) -> Self {
self.max_depth = depth;
self
}
#[must_use]
pub const fn with_work(mut self, work: usize) -> Self {
self.max_work = work;
self
}
#[must_use]
pub const fn with_findings(mut self, findings: usize) -> Self {
self.max_findings = findings;
self
}
#[must_use]
pub const fn with_output_bytes(mut self, bytes: usize) -> Self {
self.max_output_bytes = bytes;
self
}
}
impl Default for Budgets {
fn default() -> Self {
Self::default_budgets()
}
}
#[must_use]
pub struct InspectRequest<'a> {
artifact: &'a str,
subject: &'a str,
language: Option<Language>,
language_version: Option<&'a str>,
rules: RuleSet,
scope: Scope,
budgets: Budgets,
}
impl<'a> InspectRequest<'a> {
pub const fn new(artifact: &'a str, subject: &'a str) -> Self {
Self {
artifact,
subject,
language: None,
language_version: None,
rules: RuleSet::STRUCTURAL_V1,
scope: Scope::Structural,
budgets: Budgets::default_budgets(),
}
}
pub const fn language(mut self, language: Language) -> Self {
self.language = Some(language);
self
}
pub const fn language_version(mut self, version: &'a str) -> Self {
self.language_version = Some(version);
self
}
pub const fn rules(mut self, rules: RuleSet) -> Self {
self.rules = rules;
self
}
pub const fn scope(mut self, scope: Scope) -> Self {
self.scope = scope;
self
}
pub const fn budgets(mut self, budgets: Budgets) -> Self {
self.budgets = budgets;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(crate = "lgwks_std::json::serde", rename_all = "snake_case")]
#[non_exhaustive]
pub enum UnsupportedReason {
NoCompiledGrammar {
artifact: String,
},
UnknownRuleSet {
identity: String,
},
RuleSetRevision {
found: u32,
supported: u32,
},
RulesNotSupportedForLanguage {
language: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(crate = "lgwks_std::json::serde", rename_all = "snake_case")]
#[non_exhaustive]
pub enum IncompleteReason {
SourceTooLarge {
actual: usize,
limit: usize,
},
SyntaxRecovery {
recovery_nodes: usize,
diagnostics_truncated: bool,
},
NodeBudgetExceeded {
observed: usize,
limit: usize,
},
DepthBudgetExceeded {
reached: usize,
limit: usize,
},
WorkBudgetExceeded {
observed: usize,
limit: usize,
},
FindingsBudgetExceeded {
limit: usize,
},
OutputBudgetExceeded {
limit: usize,
},
ParseDeadlineExceeded {
deadline_ms: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(crate = "lgwks_std::json::serde", rename_all = "snake_case")]
#[non_exhaustive]
pub struct RuleCoverage {
rule_id: String,
supported: bool,
evaluated: bool,
evidence: String,
}
impl RuleCoverage {
#[must_use]
pub fn rule_id(&self) -> &str {
&self.rule_id
}
#[must_use]
pub const fn supported(&self) -> bool {
self.supported
}
#[must_use]
pub const fn evaluated(&self) -> bool {
self.evaluated
}
#[must_use]
pub fn evidence(&self) -> &str {
&self.evidence
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(crate = "lgwks_std::json::serde", rename_all = "snake_case")]
#[non_exhaustive]
pub struct Finding {
rule_id: String,
start_byte: usize,
end_byte: usize,
preview: String,
preview_truncated: bool,
}
impl Finding {
#[must_use]
pub fn rule_id(&self) -> &str {
&self.rule_id
}
#[must_use]
pub const fn byte_range(&self) -> (usize, usize) {
(self.start_byte, self.end_byte)
}
#[must_use]
pub fn preview(&self) -> &str {
&self.preview
}
#[must_use]
pub const fn preview_truncated(&self) -> bool {
self.preview_truncated
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(crate = "lgwks_std::json::serde", rename_all = "snake_case")]
#[non_exhaustive]
pub struct Resources {
pub nodes: usize,
pub max_depth: usize,
pub work: usize,
pub findings: usize,
pub output_bytes: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(crate = "lgwks_std::json::serde", rename_all = "snake_case")]
#[non_exhaustive]
pub enum Verdict {
Clean {
scope: Scope,
},
Violations {
count: usize,
},
Unsupported {
reason: UnsupportedReason,
},
Undecidable {
property: String,
},
Incomplete {
reason: IncompleteReason,
},
InfrastructureFailure {
cause: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(crate = "lgwks_std::json::serde", rename_all = "snake_case")]
#[non_exhaustive]
pub struct Inspection {
version: u32,
artifact: String,
subject_digest: String,
language: Option<String>,
rule_identity: String,
rule_revision: u32,
scope: Scope,
verdict: Verdict,
coverage: Vec<RuleCoverage>,
findings: Vec<Finding>,
resources: Resources,
}
impl Inspection {
#[must_use]
pub const fn version(&self) -> u32 {
self.version
}
#[must_use]
pub fn artifact(&self) -> &str {
&self.artifact
}
#[must_use]
pub fn subject_digest(&self) -> &str {
&self.subject_digest
}
#[must_use]
pub fn language(&self) -> Option<&str> {
self.language.as_deref()
}
#[must_use]
pub fn rule_identity(&self) -> &str {
&self.rule_identity
}
#[must_use]
pub const fn rule_revision(&self) -> u32 {
self.rule_revision
}
#[must_use]
pub const fn scope(&self) -> Scope {
self.scope
}
#[must_use]
pub const fn verdict(&self) -> &Verdict {
&self.verdict
}
#[must_use]
pub fn coverage(&self) -> &[RuleCoverage] {
&self.coverage
}
#[must_use]
pub fn findings(&self) -> &[Finding] {
&self.findings
}
#[must_use]
pub const fn resources(&self) -> Resources {
self.resources
}
#[must_use]
pub const fn assurance() -> &'static str {
ASSURANCE
}
pub fn to_json(&self) -> Result<String, crate::json::Error> {
crate::json::to_string(self)
}
}
struct Rule {
rule_id: &'static str,
evidence: &'static str,
}
const RULES: &[Rule] = &[
Rule {
rule_id: "rust/no-unwrap",
evidence: "an identifier named `unwrap` or `expect` (a fallible-operation panic site)",
},
Rule {
rule_id: "rust/no-todo",
evidence: "a `todo!` or `unimplemented!` macro invocation",
},
Rule {
rule_id: "rust/no-panic",
evidence: "a `panic!` macro invocation",
},
];
fn rules_speak(language: Language) -> bool {
language == Language::Rust
}
fn rule_matches(rule_id: &str, node_kind: &str, node_text: &str) -> bool {
match rule_id {
"rust/no-unwrap" => {
matches!(node_kind, "identifier" | "field_identifier")
&& matches!(node_text, "unwrap" | "expect")
}
"rust/no-todo" => {
node_kind == "macro_invocation"
&& matches!(macro_head(node_text), "todo" | "unimplemented")
}
"rust/no-panic" => node_kind == "macro_invocation" && macro_head(node_text) == "panic",
_ => false,
}
}
fn macro_head(node_text: &str) -> &str {
node_text
.split_once(['!', '(', ' ', '\t', '\n'])
.map_or(node_text, |(head, _)| head)
}
fn bounded_preview(source: &str, start: usize, end: usize) -> (String, bool) {
let Some(slice) = source.get(start..end) else {
return (String::new(), true);
};
let cutoff = slice
.char_indices()
.nth(MAX_PREVIEW_BYTES)
.map_or(slice.len(), |(at, _)| at);
let truncated = cutoff < slice.len();
(slice[..cutoff].to_owned(), truncated)
}
fn deadline_millis(duration: std::time::Duration) -> u64 {
const MILLIS_PER_SEC: u64 = 1_000;
duration
.as_secs()
.saturating_mul(MILLIS_PER_SEC)
.saturating_add(u64::from(duration.subsec_millis()))
}
type ParseFn = fn(&str, Language) -> Result<lgwks_ast::Parsed, ParseError>;
#[must_use]
pub fn inspect(request: &InspectRequest<'_>) -> Inspection {
inspect_with(request, lgwks_ast::try_parse)
}
fn inspect_with(request: &InspectRequest<'_>, parse: ParseFn) -> Inspection {
inspect_mode(request, parse, true)
}
#[cfg(test)]
fn inspect_eager(request: &InspectRequest<'_>) -> Inspection {
inspect_mode(request, lgwks_ast::try_parse, false)
}
fn inspect_mode(request: &InspectRequest<'_>, parse: ParseFn, enforce: bool) -> Inspection {
let digest = hash::blake3(request.subject.as_bytes()).to_hex();
let coverage = coverage_for(&request.rules, request.language, false);
let base = |verdict: Verdict,
language: Option<String>,
coverage: Vec<RuleCoverage>,
findings: Vec<Finding>,
resources: Resources| Inspection {
version: INSPECTION_VERSION,
artifact: request.artifact.to_owned(),
subject_digest: digest.clone(),
language,
rule_identity: request.rules.identity().to_owned(),
rule_revision: request.rules.revision(),
scope: request.scope,
verdict,
coverage,
findings,
resources,
};
let empty = Resources {
nodes: 0,
max_depth: 0,
work: 0,
findings: 0,
output_bytes: 0,
};
if request.rules.identity() != RuleSet::STRUCTURAL_V1.identity() {
return base(
Verdict::Unsupported {
reason: UnsupportedReason::UnknownRuleSet {
identity: request.rules.identity().to_owned(),
},
},
None,
coverage,
Vec::new(),
empty,
);
}
if request.rules.revision() != RuleSet::STRUCTURAL_V1.revision() {
return base(
Verdict::Unsupported {
reason: UnsupportedReason::RuleSetRevision {
found: request.rules.revision(),
supported: RuleSet::STRUCTURAL_V1.revision(),
},
},
None,
coverage,
Vec::new(),
empty,
);
}
let resolved = request
.language
.or_else(|| lgwks_ast::detect(request.artifact));
let Some(language) = resolved else {
return base(
Verdict::Unsupported {
reason: UnsupportedReason::NoCompiledGrammar {
artifact: request.artifact.to_owned(),
},
},
None,
coverage,
Vec::new(),
empty,
);
};
let language_name = language.name().to_owned();
let coverage = coverage_for(&request.rules, Some(language), false);
if let Some(version) = request.language_version {
return base(
Verdict::Undecidable {
property: format!(
"language version `{version}`: the compiled grammar carries no version \
identity, so this build will not claim to have checked that exact version"
),
},
Some(language_name),
coverage,
Vec::new(),
empty,
);
}
if !rules_speak(language) {
return base(
Verdict::Unsupported {
reason: UnsupportedReason::RulesNotSupportedForLanguage {
language: language_name.clone(),
},
},
Some(language_name),
coverage,
Vec::new(),
empty,
);
}
if request.subject.len() > request.budgets.max_source_bytes {
return base(
Verdict::Incomplete {
reason: IncompleteReason::SourceTooLarge {
actual: request.subject.len(),
limit: request.budgets.max_source_bytes,
},
},
Some(language_name),
coverage,
Vec::new(),
empty,
);
}
let parsed = match parse(request.subject, language) {
Ok(parsed) => parsed,
Err(ParseError::InvalidSyntax {
diagnostics,
diagnostics_truncated,
..
}) => {
return base(
Verdict::Incomplete {
reason: IncompleteReason::SyntaxRecovery {
recovery_nodes: diagnostics.len(),
diagnostics_truncated,
},
},
Some(language_name),
coverage,
Vec::new(),
empty,
);
}
Err(ParseError::SourceTooLarge { actual, limit }) => {
return base(
Verdict::Incomplete {
reason: IncompleteReason::SourceTooLarge { actual, limit },
},
Some(language_name),
coverage,
Vec::new(),
empty,
);
}
Err(ParseError::AstTooLarge {
observed, limit, ..
}) => {
return base(
Verdict::Incomplete {
reason: IncompleteReason::NodeBudgetExceeded { observed, limit },
},
Some(language_name),
coverage,
Vec::new(),
empty,
);
}
Err(ParseError::AstTooDeep {
observed, limit, ..
})
| Err(ParseError::ContainerNestingTooDeep {
observed, limit, ..
}) => {
return base(
Verdict::Incomplete {
reason: IncompleteReason::DepthBudgetExceeded {
reached: observed,
limit,
},
},
Some(language_name),
coverage,
Vec::new(),
empty,
);
}
Err(ParseError::TimedOut { after, .. }) => {
return base(
Verdict::Incomplete {
reason: IncompleteReason::ParseDeadlineExceeded {
deadline_ms: deadline_millis(after),
},
},
Some(language_name),
coverage,
Vec::new(),
empty,
);
}
Err(ParseError::ParserUnavailable { language, detail }) => {
return base(
Verdict::InfrastructureFailure {
cause: format!("{language} parser unavailable: {detail}"),
},
Some(language_name),
coverage,
Vec::new(),
empty,
);
}
Err(other) => {
return base(
Verdict::InfrastructureFailure {
cause: other.to_string(),
},
Some(language_name),
coverage,
Vec::new(),
empty,
);
}
};
let coverage = coverage_for(&request.rules, Some(language), true);
let walk = walk(&parsed, request.subject, &request.budgets, enforce);
let resources = Resources {
nodes: walk.nodes,
max_depth: walk.max_depth,
work: walk.work,
findings: walk.findings.len(),
output_bytes: walk.output_bytes,
};
let verdict = match walk.incomplete {
Some(reason) => Verdict::Incomplete { reason },
None if walk.findings.is_empty() => Verdict::Clean {
scope: request.scope,
},
None => Verdict::Violations {
count: walk.findings.len(),
},
};
base(
verdict,
Some(language_name),
coverage,
walk.findings,
resources,
)
}
fn coverage_for(rules: &RuleSet, language: Option<Language>, parsed: bool) -> Vec<RuleCoverage> {
let known = rules.identity() == RuleSet::STRUCTURAL_V1.identity()
&& rules.revision() == RuleSet::STRUCTURAL_V1.revision();
RULES
.iter()
.map(|rule| {
let supported = known && language.is_some_and(rules_speak);
RuleCoverage {
rule_id: rule.rule_id.to_owned(),
supported,
evaluated: supported && parsed,
evidence: rule.evidence.to_owned(),
}
})
.collect()
}
struct WalkOutcome {
nodes: usize,
max_depth: usize,
work: usize,
findings: Vec<Finding>,
output_bytes: usize,
incomplete: Option<IncompleteReason>,
}
type Frame<'t> = (lgwks_ast::AstNode<'t>, usize, usize);
fn walk(parsed: &lgwks_ast::Parsed, source: &str, budgets: &Budgets, enforce: bool) -> WalkOutcome {
let root = parsed.root();
let mut outcome = WalkOutcome {
nodes: 0,
max_depth: 0,
work: 0,
findings: Vec::new(),
output_bytes: 0,
incomplete: None,
};
if !charge_and_evaluate(&root, 1, source, budgets, &mut outcome, enforce) {
return outcome;
}
let root_children = root.children().len();
let mut frames: Vec<Frame<'_>> = vec![(root, root_children, 1)];
while let Some(frame) = frames.last_mut() {
let Some(child_index) = frame.1.checked_sub(1) else {
frames.pop();
continue;
};
frame.1 = child_index;
let next_child = frame.0.child(child_index);
let Some(child) = next_child else {
continue;
};
let depth = frame.2.saturating_add(1);
if !charge_and_evaluate(&child, depth, source, budgets, &mut outcome, enforce) {
return outcome;
}
let child_count = child.children().len();
frames.push((child, child_count, depth));
}
outcome
}
fn charge_and_evaluate(
node: &lgwks_ast::AstNode<'_>,
depth: usize,
source: &str,
budgets: &Budgets,
outcome: &mut WalkOutcome,
enforce: bool,
) -> bool {
outcome.nodes = outcome.nodes.saturating_add(1);
if enforce && outcome.nodes > budgets.max_nodes {
outcome.incomplete = Some(IncompleteReason::NodeBudgetExceeded {
observed: outcome.nodes,
limit: budgets.max_nodes,
});
return false;
}
if enforce && depth > budgets.max_depth {
outcome.max_depth = outcome.max_depth.max(depth);
outcome.incomplete = Some(IncompleteReason::DepthBudgetExceeded {
reached: depth,
limit: budgets.max_depth,
});
return false;
}
outcome.max_depth = outcome.max_depth.max(depth);
let kind = node.kind();
let node_kind: &str = kind.as_ref();
let text = node.text();
let node_text: &str = text.as_ref();
for rule in RULES {
outcome.work = outcome.work.saturating_add(1);
if enforce && outcome.work > budgets.max_work {
outcome.incomplete = Some(IncompleteReason::WorkBudgetExceeded {
observed: outcome.work,
limit: budgets.max_work,
});
return false;
}
if rule_matches(rule.rule_id, node_kind, node_text)
&& !retain_finding(rule, node, source, budgets, outcome, enforce)
{
return false;
}
}
true
}
fn retain_finding(
rule: &Rule,
node: &lgwks_ast::AstNode<'_>,
source: &str,
budgets: &Budgets,
outcome: &mut WalkOutcome,
enforce: bool,
) -> bool {
if enforce && outcome.findings.len() >= budgets.max_findings {
outcome.incomplete = Some(IncompleteReason::FindingsBudgetExceeded {
limit: budgets.max_findings,
});
return false;
}
let range = node.range();
let (preview, preview_truncated) = bounded_preview(source, range.start, range.end);
let cost = rule.rule_id.len().saturating_add(preview.len());
if enforce && outcome.output_bytes.saturating_add(cost) > budgets.max_output_bytes {
outcome.incomplete = Some(IncompleteReason::OutputBudgetExceeded {
limit: budgets.max_output_bytes,
});
return false;
}
outcome.output_bytes = outcome.output_bytes.saturating_add(cost);
outcome.findings.push(Finding {
rule_id: rule.rule_id.to_owned(),
start_byte: range.start,
end_byte: range.end,
preview,
preview_truncated,
});
true
}
#[cfg(test)]
mod tests {
use super::*;
fn wide_subject() -> String {
(0..64)
.map(|index| format!("fn f{index}() {{ let x = unwrap(); }}\n"))
.collect()
}
#[test]
fn a_parser_fault_is_an_infrastructure_failure_not_a_clean_report()
-> Result<(), Box<dyn std::error::Error>> {
let request = InspectRequest::new("src/lib.rs", "fn f() { g().unwrap(); }");
let faulting: ParseFn = |_code, language| {
Err(ParseError::ParserUnavailable {
language: language.name(),
detail: "injected fault: no compiled parser".to_owned(),
})
};
let report = inspect_with(&request, faulting);
match report.verdict().clone() {
Verdict::InfrastructureFailure { cause } => assert!(
cause.contains("injected fault"),
"the failure must carry the parser's own detail: {cause:?}"
),
other => {
return Err(format!("expected an infrastructure failure, got {other:?}").into());
}
}
assert!(
report.findings().is_empty(),
"an infrastructure failure retains no finding"
);
assert!(
!matches!(report.verdict(), Verdict::Clean { .. }),
"an infrastructure failure must never read as clean"
);
Ok(())
}
#[test]
fn a_parse_stopped_at_its_deadline_is_incomplete_not_an_infrastructure_failure()
-> Result<(), Box<dyn std::error::Error>> {
let subject = "fn f() {".repeat(4_000);
let request = InspectRequest::new("src/lib.rs", &subject);
let stopped: ParseFn =
|code, language| lgwks_ast::try_parse_within(code, language, std::time::Duration::ZERO);
let report = inspect_with(&request, stopped);
match report.verdict().clone() {
Verdict::Incomplete {
reason: IncompleteReason::ParseDeadlineExceeded { deadline_ms },
} => assert_eq!(deadline_ms, 0, "the report names the deadline applied"),
other => {
return Err(format!("expected a deadline refusal, got {other:?}").into());
}
}
assert!(
report.findings().is_empty(),
"a parse with no tree retains no finding"
);
Ok(())
}
#[test]
fn an_eager_traversal_mutant_fails_the_node_budget_oracle() {
let source = wide_subject();
let request = InspectRequest::new("wide.rs", &source).budgets(Budgets::new().with_nodes(3));
let shipped = inspect(&request);
assert!(
matches!(
shipped.verdict(),
Verdict::Incomplete {
reason: IncompleteReason::NodeBudgetExceeded { .. }
}
),
"the oracle the mutant must fail: {:?}",
shipped.verdict()
);
assert!(
shipped.resources().nodes <= 4,
"the shipped walk stops at the bound plus its overflow witness: {}",
shipped.resources().nodes
);
let mutant = inspect_eager(&request);
assert!(
!matches!(
mutant.verdict(),
Verdict::Incomplete {
reason: IncompleteReason::NodeBudgetExceeded { .. }
}
),
"the counterfactual control: the eager walk must not report the budget refusal \
the shipped walk reports: {:?}",
mutant.verdict()
);
assert!(
mutant.resources().nodes > 3,
"and it must have walked past the cap, which is what makes the oracle behavioral: {}",
mutant.resources().nodes
);
}
#[test]
fn a_subject_executing_mutant_fails_the_non_execution_oracle()
-> Result<(), Box<dyn std::error::Error>> {
static MUTANT_MARKERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
let mark = MUTANT_MARKERS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let marker = std::env::temp_dir().join(format!("lgwks-inspect-mutant-{mark}"));
let shown = marker.display();
let source = format!("fn main() {{ std::fs::remove_file(\"{shown}\").unwrap(); }}\n");
let request = InspectRequest::new("src/main.rs", &source);
std::fs::write(&marker, b"present")?;
let shipped = inspect(&request);
assert!(
marker.exists(),
"the shipped operation must not execute the subject: {:?}",
shipped.verdict()
);
let mutant = inspect_executing(&request, |_subject| {
let _removed = std::fs::remove_file(&marker);
});
assert!(
!marker.exists(),
"the executing mutant must trip the marker observer: {:?}",
mutant.verdict()
);
let _cleanup = std::fs::remove_file(&marker);
Ok(())
}
fn inspect_executing<F: FnOnce(&str)>(
request: &InspectRequest<'_>,
run_subject: F,
) -> Inspection {
run_subject(request.subject);
inspect(request)
}
}