use super::super::rust_index::{FunctionSummary, TestSummary};
use super::text::{delimited_contents_at, enum_variant_values, exact_error_variant};
use crate::domain::*;
pub(in crate::analysis) fn activation_evidence(
probe: &Probe,
owner_fn: Option<&FunctionSummary>,
related_tests: &[&TestSummary],
flow_sinks: &[FlowSinkFact],
) -> ActivationEvidence {
let mut observed_values = related_tests
.iter()
.flat_map(|test| value_facts_for_test(test, owner_fn))
.collect::<Vec<_>>();
observed_values.extend(observed_discriminator_values(
probe,
owner_fn,
related_tests,
));
sort_value_facts(&mut observed_values);
let mut missing_discriminators =
missing_discriminator_facts(probe, owner_fn, related_tests, flow_sinks, &observed_values);
missing_discriminators.sort_by(|left, right| {
left.value
.cmp(&right.value)
.then(left.reason.cmp(&right.reason))
.then(
left.flow_sink
.as_ref()
.map(|sink| sink.kind.as_str())
.cmp(&right.flow_sink.as_ref().map(|sink| sink.kind.as_str())),
)
});
missing_discriminators
.dedup_by(|left, right| left.value == right.value && left.reason == right.reason);
ActivationEvidence {
observed_values,
missing_discriminators,
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct ParameterValue {
parameter: String,
value: String,
line: usize,
text: String,
}
fn value_facts_for_test(test: &TestSummary, owner_fn: Option<&FunctionSummary>) -> Vec<ValueFact> {
let owner_name = owner_fn.map(|owner| owner.name.as_str()).unwrap_or("");
let parameters = owner_fn.map(function_parameters).unwrap_or_default();
let mut facts = Vec::new();
for call in &test.calls {
if !owner_name.is_empty() && call.name != owner_name {
continue;
}
let Some(arguments) = call_arguments(&call.text, &call.name) else {
continue;
};
for (idx, argument) in arguments.iter().enumerate() {
for value in scalar_values(argument) {
let value = parameters
.get(idx)
.map(|parameter| format!("{parameter} = {value}"))
.unwrap_or(value);
facts.push(ValueFact {
line: call.line,
text: call.text.clone(),
value,
context: ValueContext::FunctionArgument,
});
}
for value in enum_variant_values(argument) {
facts.push(ValueFact {
line: call.line,
text: call.text.clone(),
value,
context: ValueContext::EnumVariant,
});
}
}
}
for assertion in &test.assertions {
let assertion_arguments = macro_arguments(&assertion.text).unwrap_or_default();
for argument in assertion_arguments {
if argument.contains(owner_name) && !owner_name.is_empty() {
continue;
}
for value in scalar_values(&argument) {
facts.push(ValueFact {
line: assertion.line,
text: assertion.text.clone(),
value,
context: ValueContext::AssertionArgument,
});
}
}
for value in enum_variant_values(&assertion.text) {
facts.push(ValueFact {
line: assertion.line,
text: assertion.text.clone(),
value,
context: ValueContext::EnumVariant,
});
}
}
for (offset, line) in test.body.lines().enumerate() {
let line_number = test.start_line + offset;
let trimmed = line.trim();
if looks_like_table_row(trimmed) {
for value in scalar_values(trimmed) {
facts.push(ValueFact {
line: line_number,
text: trimmed.to_string(),
value,
context: ValueContext::TableRow,
});
}
}
if looks_like_builder_method(trimmed) {
for value in scalar_values(trimmed) {
facts.push(ValueFact {
line: line_number,
text: trimmed.to_string(),
value,
context: ValueContext::BuilderMethod,
});
}
}
}
sort_value_facts(&mut facts);
facts
}
fn observed_discriminator_values(
probe: &Probe,
owner_fn: Option<&FunctionSummary>,
related_tests: &[&TestSummary],
) -> Vec<ValueFact> {
let Some((left, right)) = comparison_operands(&probe.expression) else {
return Vec::new();
};
let Some(owner) = owner_fn else {
return Vec::new();
};
let parameters = function_parameters(owner);
let call_values = owner_call_parameter_values(related_tests, &owner.name, ¶meters);
let mut facts = Vec::new();
for row in call_values {
let Some(left_value) = parameter_value(&row, &left) else {
continue;
};
let right_value = parameter_value(&row, &right)
.map(|value| value.value)
.or_else(|| literal_operand_value(&right));
if right_value
.as_deref()
.is_some_and(|value| comparable_value(value) == comparable_value(&left_value.value))
{
facts.push(ValueFact {
line: left_value.line,
text: left_value.text.clone(),
value: format!("{left} == {right}"),
context: ValueContext::FunctionArgument,
});
}
}
facts
}
fn missing_discriminator_facts(
probe: &Probe,
owner_fn: Option<&FunctionSummary>,
related_tests: &[&TestSummary],
flow_sinks: &[FlowSinkFact],
observed_values: &[ValueFact],
) -> Vec<MissingDiscriminatorFact> {
let mut missing = Vec::new();
if matches!(probe.family, ProbeFamily::Predicate)
&& let Some(fact) =
missing_boundary_discriminator(probe, owner_fn, related_tests, flow_sinks)
{
missing.push(fact);
}
if (matches!(probe.family, ProbeFamily::ErrorPath)
|| flow_sinks
.iter()
.any(|sink| sink.kind == FlowSinkKind::ErrorVariant))
&& let Some(fact) = missing_error_variant_discriminator(probe, related_tests, flow_sinks)
{
missing.push(fact);
}
if missing.is_empty()
&& observed_values
.iter()
.any(|fact| fact.value.contains(" == "))
{
return Vec::new();
}
missing
}
fn missing_boundary_discriminator(
probe: &Probe,
owner_fn: Option<&FunctionSummary>,
related_tests: &[&TestSummary],
flow_sinks: &[FlowSinkFact],
) -> Option<MissingDiscriminatorFact> {
let (left, right) = comparison_operands(&probe.expression)?;
let owner = owner_fn?;
let parameters = function_parameters(owner);
let call_values = owner_call_parameter_values(related_tests, &owner.name, ¶meters);
if call_values.is_empty() {
return None;
}
let equality_observed = call_values.iter().any(|row| {
let Some(left_value) = parameter_value(row, &left) else {
return false;
};
let right_value = parameter_value(row, &right)
.map(|value| value.value)
.or_else(|| literal_operand_value(&right));
right_value
.as_deref()
.is_some_and(|value| comparable_value(value) == comparable_value(&left_value.value))
});
if equality_observed {
return None;
}
let left_values = observed_parameter_values(&call_values, &left);
let right_parameter_values = parameter_value_set(&call_values, &right);
let right_literal = literal_operand_value(&right);
let reason = if let Some(right_values) = right_parameter_values {
format!(
"No related test call uses {left} equal to {right}; observed {left} values: {}; observed {right} values: {}",
list_or_unknown(&left_values),
list_or_unknown(&right_values)
)
} else if let Some(right_value) = right_literal {
format!(
"No related test call uses {left} equal to {right}; observed {left} values: {}; target {right} value: {right_value}",
list_or_unknown(&left_values)
)
} else {
format!(
"No related test call uses {left} equal to {right}; observed {left} values: {}",
list_or_unknown(&left_values)
)
};
Some(MissingDiscriminatorFact {
value: format!("{left} == {right}"),
reason,
flow_sink: first_visible_flow_sink(flow_sinks).cloned(),
})
}
fn missing_error_variant_discriminator(
probe: &Probe,
related_tests: &[&TestSummary],
flow_sinks: &[FlowSinkFact],
) -> Option<MissingDiscriminatorFact> {
let variant = exact_error_variant(&probe.expression).or_else(|| {
flow_sinks
.iter()
.find_map(|sink| exact_error_variant(&sink.text))
})?;
let exact_assertion_found = related_tests.iter().any(|test| {
test.assertions.iter().any(|assertion| {
assertion.kind == OracleKind::ExactErrorVariant && assertion.text.contains(&variant)
})
});
if exact_assertion_found {
return None;
}
Some(MissingDiscriminatorFact {
value: variant.clone(),
reason: format!("No exact error variant assertion for {variant}"),
flow_sink: flow_sinks
.iter()
.find(|sink| sink.kind == FlowSinkKind::ErrorVariant)
.or_else(|| first_visible_flow_sink(flow_sinks))
.cloned(),
})
}
fn owner_call_parameter_values(
related_tests: &[&TestSummary],
owner_name: &str,
parameters: &[String],
) -> Vec<Vec<ParameterValue>> {
let mut rows = Vec::new();
if owner_name.is_empty() || parameters.is_empty() {
return rows;
}
for test in related_tests {
for call in &test.calls {
if call.name != owner_name {
continue;
}
let Some(arguments) = call_arguments(&call.text, &call.name) else {
continue;
};
let row = arguments
.iter()
.enumerate()
.filter_map(|(idx, argument)| {
let parameter = parameters.get(idx)?;
let value = scalar_values(argument).into_iter().next()?;
Some(ParameterValue {
parameter: parameter.clone(),
value,
line: call.line,
text: call.text.clone(),
})
})
.collect::<Vec<_>>();
if !row.is_empty() {
rows.push(row);
}
}
}
rows
}
fn parameter_value(row: &[ParameterValue], parameter: &str) -> Option<ParameterValue> {
row.iter()
.find(|value| value.parameter == parameter)
.cloned()
}
fn parameter_value_set(rows: &[Vec<ParameterValue>], parameter: &str) -> Option<Vec<String>> {
let mut values = observed_parameter_values(rows, parameter);
if values.is_empty() {
None
} else {
values.sort();
values.dedup();
Some(values)
}
}
fn observed_parameter_values(rows: &[Vec<ParameterValue>], parameter: &str) -> Vec<String> {
let mut values = rows
.iter()
.flat_map(|row| {
row.iter()
.filter(|value| value.parameter == parameter)
.map(|value| value.value.clone())
})
.collect::<Vec<_>>();
values.sort();
values.dedup();
values
}
fn function_parameters(function: &FunctionSummary) -> Vec<String> {
let signature = function
.body
.lines()
.next()
.unwrap_or(function.body.as_str());
let Some(arguments) = delimited_contents_after(signature, '(') else {
return Vec::new();
};
split_top_level_args(&arguments)
.into_iter()
.filter_map(|argument| {
argument
.split_once(':')
.map(|(name, _)| name.trim().to_string())
})
.filter(|name| !name.is_empty() && name != "self" && name != "&self" && name != "mut self")
.collect()
}
fn comparison_operands(expression: &str) -> Option<(String, String)> {
for operator in [">=", "<=", "==", "!=", ">", "<"] {
if let Some((left, right)) = expression.split_once(operator) {
let left = clean_operand(left);
let right = clean_operand(right);
if !left.is_empty() && !right.is_empty() {
return Some((left, right));
}
}
}
None
}
fn clean_operand(operand: &str) -> String {
let cleaned = operand
.trim()
.trim_start_matches("if ")
.trim_end_matches('{')
.trim_end_matches(';')
.trim();
let cleaned = cleaned
.split_once('{')
.map(|(before, _)| before.trim())
.unwrap_or(cleaned);
cleaned.to_string()
}
fn literal_operand_value(operand: &str) -> Option<String> {
scalar_values(operand).into_iter().next()
}
fn comparable_value(value: &str) -> String {
value
.trim()
.trim_matches('"')
.chars()
.filter(|ch| *ch != '_')
.collect()
}
fn first_visible_flow_sink(flow_sinks: &[FlowSinkFact]) -> Option<&FlowSinkFact> {
flow_sinks
.iter()
.find(|sink| sink.kind != FlowSinkKind::Unknown)
}
fn list_or_unknown(values: &[String]) -> String {
if values.is_empty() {
"unknown".to_string()
} else {
values.join(", ")
}
}
pub(in crate::analysis) fn has_observed_boundary_equality(activation: &ActivationEvidence) -> bool {
activation
.observed_values
.iter()
.any(|fact| fact.value.contains(" == "))
}
fn sort_value_facts(facts: &mut Vec<ValueFact>) {
facts.sort_by(|left, right| {
left.line
.cmp(&right.line)
.then(left.context.as_str().cmp(right.context.as_str()))
.then(left.value.cmp(&right.value))
.then(left.text.cmp(&right.text))
});
facts.dedup_by(|left, right| {
left.line == right.line
&& left.text == right.text
&& left.value == right.value
&& left.context == right.context
});
}
fn call_arguments(text: &str, name: &str) -> Option<Vec<String>> {
let needle = format!("{name}(");
let start = text.find(&needle)? + name.len();
let contents = delimited_contents_at(text, start)?;
Some(split_top_level_args(&contents))
}
fn macro_arguments(text: &str) -> Option<Vec<String>> {
let start = text.find("!(")? + 1;
let contents = delimited_contents_at(text, start)?;
Some(split_top_level_args(&contents))
}
fn delimited_contents_after(text: &str, delimiter: char) -> Option<String> {
let start = text.find(delimiter)?;
delimited_contents_at(text, start)
}
fn split_top_level_args(text: &str) -> Vec<String> {
let mut args = Vec::new();
let mut start = 0usize;
let mut depth = 0i32;
let mut in_string = false;
let mut escaped = false;
for (idx, ch) in text.char_indices() {
if in_string {
if escaped {
escaped = false;
} else if ch == '\\' {
escaped = true;
} else if ch == '"' {
in_string = false;
}
continue;
}
match ch {
'"' => in_string = true,
'(' | '[' | '{' => depth += 1,
')' | ']' | '}' => depth -= 1,
',' if depth == 0 => {
if let Some(arg) = text.get(start..idx).map(str::trim)
&& !arg.is_empty()
{
args.push(arg.to_string());
}
start = idx + 1;
}
_ => {}
}
}
if let Some(arg) = text.get(start..).map(str::trim)
&& !arg.is_empty()
{
args.push(arg.to_string());
}
args
}
fn scalar_values(text: &str) -> Vec<String> {
let mut values = Vec::new();
let chars = text.char_indices().collect::<Vec<_>>();
let mut idx = 0usize;
while idx < chars.len() {
let (byte_idx, ch) = chars[idx];
if ch == '"' {
let mut end = byte_idx + ch.len_utf8();
let mut cursor = idx + 1;
let mut escaped = false;
while cursor < chars.len() {
let (next_byte, next_ch) = chars[cursor];
end = next_byte + next_ch.len_utf8();
if escaped {
escaped = false;
} else if next_ch == '\\' {
escaped = true;
} else if next_ch == '"' {
break;
}
cursor += 1;
}
if let Some(value) = text.get(byte_idx..end) {
values.push(value.to_string());
}
idx = cursor.saturating_add(1);
continue;
}
if ch.is_ascii_digit()
|| (ch == '-'
&& chars
.get(idx + 1)
.is_some_and(|(_, next_ch)| next_ch.is_ascii_digit()))
{
let mut end = byte_idx + ch.len_utf8();
let mut cursor = idx + 1;
while cursor < chars.len() {
let (next_byte, next_ch) = chars[cursor];
if next_ch.is_ascii_digit() || next_ch == '_' {
end = next_byte + next_ch.len_utf8();
cursor += 1;
} else {
break;
}
}
if let Some(value) = text.get(byte_idx..end) {
values.push(value.to_string());
}
idx = cursor;
continue;
}
idx += 1;
}
values.sort();
values.dedup();
values
}
fn looks_like_table_row(line: &str) -> bool {
(line.starts_with('(') || line.starts_with('[') || line.contains("[(")) && line.contains(',')
}
fn looks_like_builder_method(line: &str) -> bool {
line.contains('.')
&& line.contains('(')
&& (line.contains("builder")
|| line.contains("with_")
|| line.contains(".amount(")
|| line.contains(".token(")
|| line.contains(".threshold("))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::rust_index::{CallFact, OracleFact};
use std::path::PathBuf;
#[test]
fn activation_evidence_records_observed_boundary_equality() {
let owner = function(
"pub fn score(amount: i32, threshold: i32) -> bool {\n amount >= threshold\n}",
);
let test = test_with_call("score_uses_boundary", "score(100, 100);");
let probe = probe(ProbeFamily::Predicate, "amount >= threshold");
let activation = activation_evidence(&probe, Some(&owner), &[&test], &[]);
assert!(has_observed_boundary_equality(&activation));
assert!(activation.missing_discriminators.is_empty());
assert!(activation.observed_values.iter().any(|fact| {
fact.context == ValueContext::FunctionArgument && fact.value == "amount == threshold"
}));
}
#[test]
fn activation_evidence_reports_missing_boundary_discriminator() {
let owner = function("pub fn score(amount: i32) -> bool {\n amount > 10\n}");
let test = test_with_call("score_uses_adjacent_value", "score(9);");
let probe = probe(ProbeFamily::Predicate, "amount > 10");
let flow_sinks = vec![FlowSinkFact {
kind: FlowSinkKind::ReturnValue,
text: "amount > 10".to_string(),
line: 2,
owner: None,
}];
let activation = activation_evidence(&probe, Some(&owner), &[&test], &flow_sinks);
assert_eq!(activation.missing_discriminators.len(), 1);
assert_eq!(activation.missing_discriminators[0].value, "amount == 10");
assert!(
activation.missing_discriminators[0]
.reason
.contains("observed amount values: 9")
);
assert_eq!(
activation.missing_discriminators[0]
.flow_sink
.as_ref()
.map(|sink| &sink.kind),
Some(&FlowSinkKind::ReturnValue)
);
}
#[test]
fn activation_evidence_omits_missing_error_variant_when_exact_assertion_exists() {
let test = test_with_assertion(
"rejects_revoked",
"assert_eq!(err, AuthError::RevokedToken);",
OracleKind::ExactErrorVariant,
);
let probe = probe(
ProbeFamily::ErrorPath,
"return Err(AuthError::RevokedToken);",
);
let flow_sinks = vec![FlowSinkFact {
kind: FlowSinkKind::ErrorVariant,
text: "Result::Err(AuthError::RevokedToken)".to_string(),
line: 2,
owner: None,
}];
let activation = activation_evidence(&probe, None, &[&test], &flow_sinks);
assert!(activation.missing_discriminators.is_empty());
}
#[test]
fn activation_evidence_sorts_multiple_missing_discriminators() {
let owner = function(
"pub fn score(amount: i32) -> Result<bool, AuthError> {\n if amount > 10 { return Err(AuthError::Bad); }\n Ok(true)\n}",
);
let test = test_with_call("score_uses_adjacent_value", "score(9);");
let probe = probe(ProbeFamily::Predicate, "amount > 10");
let flow_sinks = vec![FlowSinkFact {
kind: FlowSinkKind::ErrorVariant,
text: "Result::Err(AuthError::Bad)".to_string(),
line: 2,
owner: None,
}];
let activation = activation_evidence(&probe, Some(&owner), &[&test], &flow_sinks);
let values = activation
.missing_discriminators
.iter()
.map(|fact| fact.value.as_str())
.collect::<Vec<_>>();
assert_eq!(values, vec!["AuthError::Bad", "amount == 10"]);
}
#[test]
fn value_facts_for_test_preserves_table_builder_and_assertion_contexts() {
let test = TestSummary {
name: "table_and_builder".to_string(),
file: PathBuf::from("tests/value.rs"),
start_line: 10,
end_line: 16,
body: r#"let rows = [(99, 100), (100, 100)];
let input = Request::builder().amount(100).token("abc").build();
assert_eq!(input.amount, 100);"#
.to_string(),
calls: Vec::new(),
assertions: vec![oracle_fact(
"assert_eq!(input.amount, 100);",
OracleKind::ExactValue,
)],
literals: Vec::new(),
attrs: Vec::new(),
};
let facts = value_facts_for_test(&test, None);
assert!(
facts
.iter()
.any(|fact| fact.context == ValueContext::TableRow && fact.value == "99")
);
assert!(
facts
.iter()
.any(|fact| fact.context == ValueContext::BuilderMethod && fact.value == "100")
);
assert!(
facts
.iter()
.any(|fact| fact.context == ValueContext::BuilderMethod && fact.value == "\"abc\"")
);
assert!(
facts
.iter()
.any(|fact| fact.context == ValueContext::AssertionArgument && fact.value == "100")
);
}
#[test]
fn value_facts_for_test_filters_non_owner_calls_and_reads_enum_call_arguments() {
let owner = function(
"pub fn score(error: AuthError) -> Result<(), AuthError> {\n Err(error)\n}",
);
let test = TestSummary {
name: "enum_call".to_string(),
file: PathBuf::from("tests/value.rs"),
start_line: 10,
end_line: 12,
body: "other(AuthError::Ignored);\nscore(AuthError::RevokedToken);".to_string(),
calls: vec![
CallFact {
line: 11,
name: "other".to_string(),
text: "other(AuthError::Ignored);".to_string(),
},
CallFact {
line: 12,
name: "score".to_string(),
text: "score(AuthError::RevokedToken);".to_string(),
},
CallFact {
line: 13,
name: "score".to_string(),
text: "score;".to_string(),
},
],
assertions: Vec::new(),
literals: Vec::new(),
attrs: Vec::new(),
};
let facts = value_facts_for_test(&test, Some(&owner));
assert!(
facts
.iter()
.any(|fact| fact.value == "AuthError::RevokedToken")
);
assert!(!facts.iter().any(|fact| fact.value == "AuthError::Ignored"));
}
#[test]
fn missing_boundary_handles_missing_left_and_nonliteral_target() {
let owner = function("pub fn score(amount: i32) -> bool {\n amount > 10\n}");
let test = test_with_call("score_uses_other_value", "score(9);");
let probe = probe(ProbeFamily::Predicate, "threshold > limit");
let activation = activation_evidence(&probe, Some(&owner), &[&test], &[]);
assert_eq!(activation.missing_discriminators.len(), 1);
assert_eq!(
activation.missing_discriminators[0].value,
"threshold == limit"
);
assert!(
activation.missing_discriminators[0]
.reason
.contains("observed threshold values: unknown")
);
}
#[test]
fn owner_call_parameter_values_handles_empty_inputs_and_skips_other_calls() {
let test = TestSummary {
name: "mixed_calls".to_string(),
file: PathBuf::from("tests/value.rs"),
start_line: 10,
end_line: 12,
body: "other(1);\nscore(2);".to_string(),
calls: vec![
CallFact {
line: 11,
name: "other".to_string(),
text: "other(1);".to_string(),
},
CallFact {
line: 12,
name: "score".to_string(),
text: "score(2);".to_string(),
},
CallFact {
line: 13,
name: "score".to_string(),
text: "score;".to_string(),
},
],
assertions: Vec::new(),
literals: Vec::new(),
attrs: Vec::new(),
};
assert!(owner_call_parameter_values(&[&test], "", &["amount".to_string()]).is_empty());
assert!(owner_call_parameter_values(&[&test], "score", &[]).is_empty());
let rows = owner_call_parameter_values(&[&test], "score", &["amount".to_string()]);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0][0].value, "2");
}
#[test]
fn text_helpers_handle_braces_escapes_negative_numbers_and_dedup_contexts() {
let owner = function("pub fn score(amount: i32) -> bool {\n amount > 10\n}");
let non_comparison_probe = probe(ProbeFamily::ReturnValue, "amount");
assert!(observed_discriminator_values(&non_comparison_probe, Some(&owner), &[]).is_empty());
assert!(
observed_discriminator_values(&probe(ProbeFamily::Predicate, "amount > 10"), None, &[])
.is_empty()
);
assert_eq!(
comparison_operands("if amount > 10 {"),
Some(("amount".to_string(), "10".to_string()))
);
assert_eq!(comparison_operands("> 10"), None);
assert_eq!(
call_arguments(r#"score("a\",b", -12)"#, "score"),
Some(vec![r#""a\",b""#.to_string(), "-12".to_string()])
);
assert_eq!(
scalar_values(r#""a\"b" -12"#),
vec!["\"a\\\"b\"".to_string(), "-12".to_string()]
);
let mut facts = vec![
value_fact(1, "score(1)", "1", ValueContext::FunctionArgument),
value_fact(1, "score(1)", "1", ValueContext::AssertionArgument),
];
sort_value_facts(&mut facts);
assert_eq!(facts.len(), 2);
}
fn probe(family: ProbeFamily, expression: &str) -> Probe {
Probe {
id: ProbeId("probe:src/lib.rs:2:score".to_string()),
location: SourceLocation::new("src/lib.rs", 2, 5),
owner: None,
family,
delta: DeltaKind::Control,
before: None,
after: Some(expression.to_string()),
expression: expression.to_string(),
expected_sinks: Vec::new(),
required_oracles: Vec::new(),
}
}
fn function(body: &str) -> FunctionSummary {
FunctionSummary {
id: SymbolId("src/lib.rs::score".to_string()),
name: "score".to_string(),
file: PathBuf::from("src/lib.rs"),
start_line: 1,
end_line: 3,
body: body.to_string(),
calls: Vec::new(),
returns: Vec::new(),
literals: Vec::new(),
is_test: false,
attrs: Vec::new(),
}
}
fn test_with_call(name: &str, call: &str) -> TestSummary {
TestSummary {
name: name.to_string(),
file: PathBuf::from("tests/score.rs"),
start_line: 10,
end_line: 12,
body: call.to_string(),
calls: vec![CallFact {
name: "score".to_string(),
line: 11,
text: call.to_string(),
}],
assertions: Vec::new(),
literals: Vec::new(),
attrs: Vec::new(),
}
}
fn test_with_assertion(name: &str, assertion: &str, kind: OracleKind) -> TestSummary {
TestSummary {
name: name.to_string(),
file: PathBuf::from("tests/score.rs"),
start_line: 10,
end_line: 12,
body: assertion.to_string(),
calls: Vec::new(),
assertions: vec![oracle_fact(assertion, kind)],
literals: Vec::new(),
attrs: Vec::new(),
}
}
fn oracle_fact(assertion: &str, kind: OracleKind) -> OracleFact {
OracleFact {
kind,
strength: OracleStrength::Strong,
line: 11,
text: assertion.to_string(),
observed_tokens: Vec::new(),
}
}
fn value_fact(line: usize, text: &str, value: &str, context: ValueContext) -> ValueFact {
ValueFact {
line,
text: text.to_string(),
value: value.to_string(),
context,
}
}
}