#[derive(serde::Deserialize)]
struct SourcePair {
id: String,
a: String,
b: String,
}
use echoes::{DocTest, Echoes, Kind, Remedy, SourceFile, TargetScan, group};
#[path = "support/doc_blocks.rs"]
mod doc_blocks;
#[path = "support/function_ids.rs"]
mod functions;
#[path = "support/group_members.rs"]
mod group_members;
#[path = "support/same.rs"]
mod same;
#[path = "support/site_src.rs"]
mod site_src;
use doc_blocks::doc_blocks;
use functions::function_ids;
use group_members::members;
use same::same;
use site_src::site;
fn target(text: &str) -> TargetScan {
TargetScan {
name: "structure".to_owned(),
files: vec![SourceFile {
path: "src/lib.rs".to_owned(),
segments: Vec::new(),
cfg: Vec::new(),
parsed: syn::parse_file(text).unwrap(),
text: text.to_owned(),
rustdoc: true,
}],
library: true,
}
}
#[test]
fn structure_cli_fixture_preserves_exact_function_and_doctest_members() {
let text = include_str!("fixtures/structurews/src/lib.rs");
let target = target(text);
let report = Echoes::default().functions().fn_min_tokens(0).run_targets(
"",
std::slice::from_ref(&target),
&|_, _| None,
);
let function_groups: Vec<_> = report
.groups
.iter()
.filter(|group| group.kind == Kind::Function)
.collect();
let function_members = members(function_groups.iter().copied());
assert_eq!(
function_members,
[
vec!["structure::arms::first", "structure::arms::same"],
vec!["structure::fields::first", "structure::fields::same"],
]
);
for group in &function_groups {
assert_eq!(group.remedy, Some(Remedy::Delete));
assert!(group.sites.iter().all(|site| {
site.file == "src/lib.rs" && site.line > 0 && site.end.is_some() && site.public
}));
}
let doctest_groups: Vec<_> = report
.groups
.iter()
.filter(|group| group.kind == Kind::Doctest)
.collect();
assert_eq!(doctest_groups.len(), 1);
let sites = &doctest_groups[0].sites;
assert_eq!(sites.len(), 2);
assert!(sites.iter().all(|site| {
site.item == "structure" && site.info.is_empty() && site.file == "src/lib.rs"
}));
assert_ne!(sites[0].line, sites[1].line);
assert!(doctest_groups[0].remedy.is_none());
let grouped: Vec<&DocTest> = report
.groups
.iter()
.flat_map(|group| group.sites.iter())
.collect();
assert_eq!(
grouped
.iter()
.filter(|site| site.item == "structure")
.count(),
2
);
assert!(
!grouped
.iter()
.any(|site| { site.info.iter().any(|tag| tag == "should_panic") })
);
}
#[test]
fn structure_cli_fixture_boundaries_and_reversed_sites() {
let text = include_str!("fixtures/structurews/src/lib.rs");
let mut target = target(text);
let base = Echoes::default().functions().fn_min_tokens(0).run_targets(
"",
std::slice::from_ref(&target),
&|_, _| None,
);
let function_groups: Vec<_> = base
.groups
.iter()
.filter(|group| group.kind == Kind::Function)
.collect();
assert_eq!(function_groups.len(), 2);
for group in &function_groups {
let prefixes: Vec<&str> = group
.sites
.iter()
.map(|site| {
let item = site.item.as_str();
&item[..item.rfind("::").unwrap()]
})
.collect();
assert!(
prefixes.iter().all(|prefix| *prefix == prefixes[0]),
"one module per group"
);
}
target.files[0].parsed.items.reverse();
let reversed_scan = Echoes::default().functions().fn_min_tokens(0).run_targets(
"",
std::slice::from_ref(&target),
&|_, _| None,
);
assert_eq!(function_ids(&base), function_ids(&reversed_scan));
let doctest_sites: Vec<DocTest> = base
.groups
.iter()
.filter(|group| group.kind == Kind::Doctest)
.flat_map(|group| group.sites.iter().cloned())
.collect();
assert_eq!(doctest_sites.len(), 2);
assert!(doctest_sites.iter().all(|site| site.info.is_empty()));
let reversed_sites: Vec<DocTest> = doctest_sites.iter().rev().cloned().collect();
let included = group(&doctest_sites, 1, 0);
let reversed_doctests = group(&reversed_sites, 1, 0);
assert_eq!(included.groups.len(), 1);
assert_eq!(
members(&included.groups),
members(&reversed_doctests.groups)
);
assert_eq!(included.groups[0].id, reversed_doctests.groups[0].id);
}
fn find_function<'a>(file: &'a syn::File, module: &str, name: &str) -> &'a syn::ItemFn {
for item in &file.items {
if let syn::Item::Mod(mod_) = item
&& mod_.ident == module
&& let Some((_, items)) = &mod_.content
{
for inner in items {
if let syn::Item::Fn(function) = inner
&& function.sig.ident == name
{
return function;
}
}
}
}
panic!("fixture is missing {module}::{name}");
}
#[test]
fn structure_threshold_uses_public_canonical_sizes() {
let text = include_str!("fixtures/structurews/src/lib.rs");
let file = syn::parse_file(text).unwrap();
let entries: [(&[String], &syn::File); 1] = [(&[], &file)];
let context = syn_canon::SourceContext::new(entries);
let target = target(text);
let base = Echoes::default().functions().fn_min_tokens(0).run_targets(
"",
std::slice::from_ref(&target),
&|_, _| None,
);
let function_groups: Vec<_> = base
.groups
.iter()
.filter(|group| group.kind == Kind::Function)
.collect();
assert_eq!(function_groups.len(), 2);
for group in &function_groups {
for site in &group.sites {
let item = site.item.as_str();
let (module, name) = item
.split_once("::")
.and_then(|(_, rest)| rest.rsplit_once("::"))
.unwrap();
let function = find_function(&file, module, name);
let size = context
.function(&function.sig, &function.block)
.expect("the site is indexed")
.canonicalize()
.body_units();
assert_eq!(group.tokens, size, "{item}");
}
let floor = group.tokens;
let included = Echoes::default()
.functions()
.fn_min_tokens(floor)
.run_targets("", std::slice::from_ref(&target), &|_, _| None);
let excluded = Echoes::default()
.functions()
.fn_min_tokens(floor + 1)
.run_targets("", std::slice::from_ref(&target), &|_, _| None);
assert!(included.groups.iter().any(|member| member.id == group.id));
assert!(!excluded.groups.iter().any(|member| member.id == group.id));
}
let blocks = doc_blocks(text);
assert_eq!(blocks.len(), 4);
let first_form = syn_canon::canonicalize(syn::parse_str::<syn::File>(&blocks[0]).unwrap());
let second_form = syn_canon::canonicalize(syn::parse_str::<syn::File>(&blocks[1]).unwrap());
let third_form = syn_canon::canonicalize(syn::parse_str::<syn::File>(&blocks[2]).unwrap());
let fourth_form = syn_canon::canonicalize(syn::parse_str::<syn::File>(&blocks[3]).unwrap());
assert_eq!(first_form, second_form);
assert_ne!(first_form, third_form);
assert_ne!(second_form, fourth_form);
let tokens = first_form.leaf_tokens();
assert_eq!(second_form.leaf_tokens(), tokens);
let doctest_groups: Vec<_> = base
.groups
.iter()
.filter(|group| group.kind == Kind::Doctest)
.collect();
assert_eq!(doctest_groups[0].tokens, tokens);
let sites = [
site("first", &blocks[0], &[]),
site("second", &blocks[1], &[]),
];
let reversed = [
site("second", &blocks[1], &[]),
site("first", &blocks[0], &[]),
];
let included = group(&sites, 2, tokens);
let reversed_report = group(&reversed, 2, tokens);
assert_eq!(members(&included.groups), [vec!["first", "second"]]);
assert_eq!(members(&reversed_report.groups), members(&included.groups));
assert_eq!(included.groups[0].id, reversed_report.groups[0].id);
assert_eq!(included.groups[0].tokens, tokens);
assert_eq!(group(&sites, 2, tokens + 1).groups.len(), 0);
let changed = group(
&[
site("changed", &blocks[2], &[]),
site("first", &blocks[0], &[]),
],
2,
tokens,
);
assert_eq!(changed.groups.len(), 0);
let flagged = group(
&[
site("first", &blocks[0], &[]),
site("second", &blocks[1], &[]),
site("flag_first", &blocks[0], &["should_panic"]),
site("flag_second", &blocks[1], &["should_panic"]),
site("failing_first", &blocks[0], &["compile_fail"]),
site("failing_second", &blocks[1], &["compile_fail"]),
],
2,
tokens,
);
assert_eq!(
members(&flagged.groups),
[vec!["first", "second"], vec!["flag_first", "flag_second"],]
);
}
fn form(source: &str) -> syn_canon::CanonicalForm {
syn_canon::canonicalize(syn::parse_str(source).unwrap())
}
fn failing(source: &str) -> syn_canon::CanonicalForm {
syn_canon::canonicalize_failing(syn::parse_str(source).unwrap())
}
fn distinct(left: &str, right: &str) {
assert_ne!(form(left), form(right), "{left}\n{right}");
}
#[test]
fn structure_canonical_folds_keep_labels_and_effects() {
same(
"struct S{x:u32,y:u32}\nfn f(input:u32)->(u32,u32){let s=S{x:input&15u32,y:input>>4};(s.x,s.y)}",
"struct S{x:u32,y:u32}\nfn f(input:u32)->(u32,u32){let s=S{y:input>>4,x:input&15u32};(s.x,s.y)}",
);
distinct(
"struct S{x:u32,y:u32}\nfn f(input:u32)->(u32,u32){let s=S{x:input&15u32,y:input>>4};(s.x,s.y)}",
"struct S{x:u32,y:u32}\nfn f(input:u32)->(u32,u32){let s=S{x:input>>4,y:input&15u32};(s.x,s.y)}",
);
same(
"struct S{x:u32,y:u32}\nfn base(input:u32)->S{S{x:input,y:input}}\nfn f(input:u32)->(u32,u32){let s=S{x:input&15u32,y:input>>4,..base(input)};(s.x,s.y)}",
"struct S{x:u32,y:u32}\nfn base(input:u32)->S{S{x:input,y:input}}\nfn f(input:u32)->(u32,u32){let s=S{y:input>>4,x:input&15u32,..base(input)};(s.x,s.y)}",
);
distinct(
"struct S(u32,u32);\nfn f(input:u32)->(u32,u32){let s=S(input&15u32,input>>4);(s.0,s.1)}",
"struct S(u32,u32);\nfn f(input:u32)->(u32,u32){let s=S(input>>4,input&15u32);(s.0,s.1)}",
);
let cases: Vec<SourcePair> =
serde_json::from_str(include_str!("fixtures/dependency/structure_cases.json")).unwrap();
let shadow = cases
.iter()
.find(|case| case.id == "S10_alias_shadowed_fields")
.unwrap();
distinct(&shadow.a, &shadow.b);
assert_ne!(failing(&shadow.a), failing(&shadow.b));
same(
"fn note(input:u32)->u32{input}\nstruct S{x:u32,y:u32}\nfn f(input:u32)->(u32,u32){let s=S{x:input&15u32,y:note(input>>4)};(s.x,s.y)}",
"fn note(input:u32)->u32{input}\nstruct S{x:u32,y:u32}\nfn f(input:u32)->(u32,u32){let s=S{y:note(input>>4),x:input&15u32};(s.x,s.y)}",
);
distinct(
"fn fail(input:u32)->u32{if input>10u32{panic!(\"no\")}input}\nstruct S{x:u32,y:u32}\nfn f(input:u32)->(u32,u32){let s=S{x:input&15u32,y:fail(input>>4)};(s.x,s.y)}",
"fn fail(input:u32)->u32{if input>10u32{panic!(\"no\")}input}\nstruct S{x:u32,y:u32}\nfn f(input:u32)->(u32,u32){let s=S{y:fail(input>>4),x:input&15u32};(s.x,s.y)}",
);
distinct(
"fn fx(input:u32)->u32{println!(\"fx\");input&7u32}\nstruct S{x:u32,y:u32}\nfn f(input:u32)->u32{let s=S{x:fx(input),y:input>>4};s.x}",
"fn fx(input:u32)->u32{println!(\"fx\");input&7u32}\nstruct S{x:u32,y:u32}\nfn f(input:u32)->u32{let s=S{y:input>>4,x:fx(input)};s.x}",
);
}
#[test]
fn structure_canonical_alternatives_keep_payload_and_order() {
same(
"fn f(input:u32)->bool{match input{1|2|3=>true,_=>false}}",
"fn f(input:u32)->bool{match input{3|1|2=>true,_=>false}}",
);
same(
"fn f(input:u32)->bool{match input{2u32|1u32=>true,_=>false}}",
"fn f(input:u32)->bool{match input{1u32|2u32=>true,_=>false}}",
);
distinct(
"fn f(input:u32)->bool{match input{1|1|2=>true,_=>false}}",
"fn f(input:u32)->bool{match input{1|2=>true,_=>false}}",
);
distinct(
"fn f(input:u32)->bool{match input{1|2 if input!=0=>true,_=>false}}",
"fn f(input:u32)->bool{match input{2|1 if input!=0=>true,_=>false}}",
);
distinct(
"fn f(input:u32)->u32{match input{n@1|n@2=>n,_=>0u32}}",
"fn f(input:u32)->u32{match input{n@2|n@1=>n,_=>0u32}}",
);
distinct(
"fn f(input:u32)->bool{match input{1|2..=2=>true,_=>false}}",
"fn f(input:u32)->bool{match input{2..=2|1=>true,_=>false}}",
);
distinct(
"macro_rules!two{()=>{2u32}}\nfn f(input:u32)->bool{match input{two!()|1=>true,_=>false}}",
"macro_rules!two{()=>{2u32}}\nfn f(input:u32)->bool{match input{1|two!()=>true,_=>false}}",
);
}
#[test]
fn structure_native_domains_and_observed_bases() {
let cases: Vec<SourcePair> =
serde_json::from_str(include_str!("fixtures/dependency/structure_cases.json")).unwrap();
for id in [
"S19_effectful_update_base",
"S20_panic_update_base",
"S21_bool_alternatives",
"S22_char_alternatives",
"S23_byte_alternatives",
"S24_string_alternatives",
"S25_u8_fields",
"S26_u16_fields",
"S27_u32_fields",
"S28_u64_fields",
"S29_u128_fields",
"S30_i8_fields",
"S31_i16_fields",
"S32_i32_fields",
"S33_i64_fields",
"S34_i128_fields",
] {
let case = cases.iter().find(|case| case.id == id).unwrap();
same(&case.a, &case.b);
}
let observer = cases
.iter()
.find(|case| case.id == "S35_literal_payload_observer")
.unwrap();
distinct(&observer.a, &observer.b);
assert_ne!(failing(&observer.a), failing(&observer.b));
}