use super::*;
use super::super::name_hint::{name_hint_rows, stem_matches, NAME_HINT_REFERENCE_CAP};
use crate::graph::Edge;
fn index_over<'g>(root: &Path, g: &'g graph::Graph) -> GraphIndex<'g> {
load_graph_index(g, root)
}
fn refers(file: &str, line: usize, to: &str) -> Edge {
Edge::UsesType {
from_file: file.to_string(),
from_line: line,
to: to.to_string(),
to_file: "Src/Target.cs".to_string(),
heuristic: false,
}
}
fn test_type(id: &str, file: &str) -> graph::Def {
let mut d = def(id, last_segment(id), "Tests", "class", file, 1);
d.test_methods = vec!["Runs".to_string()];
d
}
fn last_segment(id: &str) -> &str {
id.rsplit('.').next().unwrap_or(id)
}
#[test]
fn stem_match_requires_one_affix_and_ignores_case_and_separators() {
for file in [
"Tests/FooTests.cs",
"Tests/FooTest.cs",
"Tests/footests.cs",
"Tests/foo.spec.ts",
"Tests/foo.test.ts",
] {
assert!(stem_matches("Foo", file), "{file} names Foo");
}
for file in [
"Tests/foo-bar.spec.ts",
"Tests/foo_bar_test.rs",
"Tests/FooBarTests.cs",
] {
assert!(stem_matches("FooBar", file), "{file} names FooBar");
}
}
#[test]
fn stem_match_refuses_a_file_that_only_contains_the_name() {
for file in [
"Tests/Foo.cs",
"Tests/FooBarTests.cs",
"Tests/BarFooTests.cs",
"Tests/FooTestsHelper.cs",
"Tests/Fooo.cs",
] {
assert!(!stem_matches("Foo", file), "{file} does not name Foo");
}
assert!(stem_matches("Foo-Bar", "Tests/FooBarTest.cs"));
}
#[test]
fn stem_match_reads_the_last_segment_so_a_matching_directory_is_not_enough() {
assert!(!stem_matches("Foo", "Foo/Helpers/Harness.cs"));
}
#[test]
fn stem_match_lets_only_a_name_that_normalizes_to_nothing_match_a_bare_affix() {
assert!(stem_matches("-", "Tests/Tests.cs"));
assert!(stem_matches("_", "Tests/Spec.cs"));
assert!(!stem_matches("Foo", "Tests/Tests.cs"));
}
#[test]
fn stem_match_lowercases_the_whole_name_so_the_rule_is_not_ascii_only() {
assert!(stem_matches("Ünicode", "Tests/ÜnicodeTests.cs"));
assert!(stem_matches("Ünicode", "Tests/ünicodetests.cs"));
assert!(!stem_matches("Ünicode", "Tests/UnicodeTests.cs"));
}
#[test]
fn the_rule_reads_the_def_simple_name_and_not_the_query_the_caller_typed() {
let root = temp_repo_root("name-hint-qualified");
let g = make_graph(
vec![
def("Outer.Foo", "Foo", "Outer", "class", "Src/Foo.cs", 1),
test_type("Tests.FooTests", "Tests/FooTests.cs"),
],
Vec::new(),
);
let index = index_over(&root, &g);
let TestsResult::Resolved(model) = build_tests_model(&index, "Outer.Foo") else {
panic!("Outer.Foo resolves");
};
assert_eq!(model.name_hint.len(), 1, "{model:?}");
assert_eq!(model.name_hint[0].file, "Tests/FooTests.cs");
}
#[test]
fn a_matching_test_file_names_the_production_symbols_it_references() {
let root = temp_repo_root("name-hint-refs");
let g = make_graph(
vec![
def("Prod.Foo", "Foo", "Prod", "class", "Src/Foo.cs", 1),
def("Prod.Bar", "Bar", "Prod", "class", "Src/Bar.cs", 1),
test_type("Tests.FooTests", "Tests/FooTests.cs"),
],
vec![refers("Tests/FooTests.cs", 5, "Prod.Bar")],
);
let index = index_over(&root, &g);
let rows = name_hint_rows(&index, "Prod.Foo");
assert_eq!(rows.len(), 1, "{rows:?}");
assert_eq!(rows[0].file, "Tests/FooTests.cs");
assert_eq!(rows[0].references, vec!["Prod.Bar".to_string()]);
assert_eq!(rows[0].reference_total, 1);
}
#[test]
fn a_symbol_its_own_test_file_cannot_hint_is_the_declaring_file() {
let root = temp_repo_root("name-hint-own");
let g = make_graph(
vec![
def("Prod.Foo", "Foo", "Prod", "class", "Tests/FooTests.cs", 1),
def(
"Tests.FooTests",
"FooTests",
"Tests",
"class",
"Tests/FooTests.cs",
1,
),
],
Vec::new(),
);
let index = index_over(&root, &g);
assert!(
name_hint_rows(&index, "Prod.Foo").is_empty(),
"a symbol declared in the matching file is not hinted at itself"
);
}
#[test]
fn a_matching_file_no_vouch_reaches_is_not_a_candidate() {
let root = temp_repo_root("name-hint-unvouched");
let g = make_graph(
vec![
def("Prod.Foo", "Foo", "Prod", "class", "Src/Foo.cs", 1),
def("Other.Bar", "Bar", "Other", "class", "Other/Bar.cs", 1),
],
vec![refers("Tests/FooTests.cs", 5, "Other.Bar")],
);
let index = index_over(&root, &g);
assert!(name_hint_rows(&index, "Prod.Foo").is_empty());
}
#[test]
fn references_rank_by_count_then_id_and_report_the_total_behind_the_cap() {
let root = temp_repo_root("name-hint-cap");
let mut defs = vec![def("Prod.Foo", "Foo", "Prod", "class", "Src/Foo.cs", 1)];
let mut edges: Vec<Edge> = Vec::new();
for (i, name) in (0..7).rev().enumerate() {
let id = format!("Prod.T{name}");
defs.push(def(
&id,
&format!("T{name}"),
"Prod",
"class",
"Src/T.cs",
1,
));
for _ in 0..=i {
edges.push(refers("Tests/FooTests.cs", 5, &id));
}
}
let tie_a = "Prod.A".to_string();
let tie_b = "Prod.B".to_string();
defs.push(def(&tie_a, "A", "Prod", "class", "Src/A.cs", 1));
defs.push(def(&tie_b, "B", "Prod", "class", "Src/B.cs", 1));
edges.push(refers("Tests/FooTests.cs", 5, &tie_a));
edges.push(refers("Tests/FooTests.cs", 5, &tie_b));
defs.push(test_type("Tests.FooTests", "Tests/FooTests.cs"));
defs.push(test_type("Tests.Helper", "Tests/Helper.cs"));
edges.push(refers("Tests/FooTests.cs", 6, "Tests.Helper"));
edges.push(Edge::UsesType {
from_file: "Tests/FooTests.cs".to_string(),
from_line: 7,
to: "Prod.Guessed".to_string(),
to_file: "Src/Guessed.cs".to_string(),
heuristic: true,
});
defs.push(def(
"Prod.Guessed",
"Guessed",
"Prod",
"class",
"Src/Guessed.cs",
1,
));
let g = make_graph(defs, edges);
let index = index_over(&root, &g);
let rows = name_hint_rows(&index, "Prod.Foo");
assert_eq!(rows.len(), 1, "{rows:?}");
let row = &rows[0];
assert_eq!(row.references.len(), NAME_HINT_REFERENCE_CAP, "{row:?}");
assert_eq!(
row.references,
vec![
"Prod.T0".to_string(),
"Prod.T1".to_string(),
"Prod.T2".to_string(),
"Prod.T3".to_string(),
"Prod.T4".to_string(),
]
);
assert_eq!(
row.reference_total, 9,
"the two tied ids count too: {row:?}"
);
assert!(!row.references.contains(&"Tests.Helper".to_string()));
assert!(!row.references.contains(&"Prod.Guessed".to_string()));
}
#[test]
fn a_matching_file_with_no_production_reference_is_listed_empty() {
let root = temp_repo_root("name-hint-empty");
let g = make_graph(
vec![
def("Prod.Foo", "Foo", "Prod", "class", "Src/Foo.cs", 1),
test_type("Tests.FooTests", "Tests/FooTests.cs"),
test_type("Tests.Helper", "Tests/Helper.cs"),
],
vec![refers("Tests/FooTests.cs", 5, "Tests.Helper")],
);
let index = index_over(&root, &g);
let rows = name_hint_rows(&index, "Prod.Foo");
assert_eq!(rows.len(), 1, "{rows:?}");
assert!(rows[0].references.is_empty(), "{rows:?}");
assert_eq!(rows[0].reference_total, 0);
}
#[test]
fn rows_follow_path_order() {
let root = temp_repo_root("name-hint-order");
let g = make_graph(
vec![
def("Prod.Foo", "Foo", "Prod", "class", "Src/Foo.cs", 1),
test_type("Tests.BFooTests", "Tests/B/FooTests.cs"),
test_type("Tests.AFooTests", "Tests/A/FooTests.cs"),
],
Vec::new(),
);
let index = index_over(&root, &g);
let rows = name_hint_rows(&index, "Prod.Foo");
assert_eq!(
rows.iter().map(|r| r.file.as_str()).collect::<Vec<_>>(),
vec!["Tests/A/FooTests.cs", "Tests/B/FooTests.cs"]
);
}
#[test]
fn an_answer_with_a_row_or_a_possible_route_carries_no_hint() {
let root = temp_repo_root("name-hint-suppressed");
let g = make_graph(
vec![
def("Prod.Foo", "Foo", "Prod", "class", "Src/Foo.cs", 1),
test_type("Tests.FooTests", "Tests/FooTests.cs"),
],
vec![refers("Tests/FooTests.cs", 5, "Prod.Foo")],
);
let index = index_over(&root, &g);
let TestsResult::Resolved(model) = build_tests_model(&index, "Prod.Foo") else {
panic!("Foo resolves");
};
assert_eq!(model.test_file_count, 1);
assert!(
model.name_hint.is_empty(),
"a real reference is an answer, so no name observation joins it"
);
}
#[test]
fn a_test_file_reached_only_over_a_bus_hop_suppresses_the_hint() {
let root = temp_repo_root("name-hint-bus");
let g = make_graph(
vec![
def("Prod.Foo", "Foo", "Prod", "class", "Src/Foo.cs", 1),
test_type("Tests.FooTests", "Tests/FooTests.cs"),
],
vec![Edge::BusHop {
from_file: "Tests/FooTests.cs".to_string(),
from_line: 3,
message: "Ping".to_string(),
to: "Prod.Foo".to_string(),
to_file: "Src/Foo.cs".to_string(),
evidence: "publish-site".to_string(),
}],
);
let index = index_over(&root, &g);
let TestsResult::Resolved(model) = build_tests_model(&index, "Prod.Foo") else {
panic!("Foo resolves");
};
assert_eq!(model.bus.total, 1);
assert!(model.rows.is_empty());
assert!(
model.name_hint.is_empty(),
"a possible route is still an answer, so no name observation joins it"
);
}