use super::*;
use std::collections::HashMap;
use std::fs;
mod qualified_path_tokens;
mod qualified_receiver;
#[test]
fn rebuild_graph_writes_the_current_caches_and_deletes_every_superseded_generation() {
let dir = temp_dir("rebuild-v13");
fs::create_dir_all(graph_dir(&dir)).unwrap();
assert_eq!(SUPERSEDED_CACHE_FILES.len(), 86, "v1..v43 pairs");
for stale in SUPERSEDED_CACHE_FILES {
fs::write(graph_dir(&dir).join(stale), b"{}").unwrap();
}
let fragment = Fragment {
defs: vec![FragDef {
id: "App.A".into(),
name: "A".into(),
namespace: "App".into(),
kind: "class".into(),
line: 1,
methods: vec![],
properties: vec![],
fields: vec![],
method_returns: OrderedMap::new(),
extension_methods: vec![],
bases: vec![],
type_params: vec![],
base_generic_args: OrderedMap::new(),
test_methods: vec![],
property_types: OrderedMap::new(),
field_types: OrderedMap::new(),
method_return_args: OrderedMap::new(),
non_public_methods: vec![],
method_arities: OrderedMap::new(),
method_type_arities: OrderedMap::new(),
method_type_param_sites: OrderedMap::new(),
method_params: OrderedMap::new(),
override_methods: vec![],
base_type_args: OrderedMap::new(),
property_message_args: Vec::new(),
array_message_bases: Vec::new(),
delegate_signature: None,
end_line: 1,
}],
usings: vec![],
refs: vec![],
names: vec![],
registrations: vec![],
publishes: vec![],
handler_registrations: vec![],
transport: vec![],
};
let mut fresh = HashMap::new();
fresh.insert("src/A.cs".to_string(), AnyFragment::Cs(fragment));
let graph_files = vec![GraphFile {
rel: "src/A.cs".to_string(),
mtime: 222,
}];
rebuild_graph(&dir, &graph_files, &fresh, true, None, None).unwrap();
assert!(
graph_dir(&dir).join("fragments-v44.json").exists(),
"the v44 payload cache is what gets written"
);
assert!(
graph_dir(&dir).join("fragments-index-v44.json").exists(),
"and its key-only index alongside it"
);
for stale in SUPERSEDED_CACHE_FILES {
assert!(
!graph_dir(&dir).join(stale).exists(),
"{stale} must be deleted -- rename IS the invalidation"
);
}
}
#[test]
fn fragments_cache_v44_supersedes_earlier_generations() {
let dir = temp_dir("fragments-cache-v44-paths");
assert_eq!(
fragments_cache_path(&dir),
graph_dir(&dir).join("fragments-v44.json")
);
assert_eq!(
fragments_index_path(&dir),
graph_dir(&dir).join("fragments-index-v44.json")
);
assert!(
SUPERSEDED_CACHE_FILES.contains(&"fragments-v26.json"),
"v26 joined the superseded list when the v29 bump landed"
);
assert!(
SUPERSEDED_CACHE_FILES.contains(&"fragments-index-v26.json"),
"its index pairs with it, same as every other generation"
);
for generation in [
"v27", "v28", "v29", "v30", "v31", "v32", "v33", "v34", "v35", "v36", "v37", "v38", "v39",
"v40", "v41", "v42", "v43",
] {
assert!(SUPERSEDED_CACHE_FILES.contains(&format!("fragments-{generation}.json").as_str()));
assert!(
SUPERSEDED_CACHE_FILES.contains(&format!("fragments-index-{generation}.json").as_str())
);
}
let dir = temp_dir("rebuild-v44");
fs::create_dir_all(graph_dir(&dir)).unwrap();
for earlier in ["v40", "v41"] {
fs::write(
graph_dir(&dir).join(format!("fragments-{earlier}.json")),
b"{}",
)
.unwrap();
fs::write(
graph_dir(&dir).join(format!("fragments-index-{earlier}.json")),
b"{}",
)
.unwrap();
}
let fragment = Fragment {
defs: vec![],
usings: vec![],
refs: vec![],
names: vec![],
registrations: vec![],
publishes: vec![],
handler_registrations: vec![],
transport: vec![],
};
let mut fresh = HashMap::new();
fresh.insert("src/A.cs".to_string(), AnyFragment::Cs(fragment));
let graph_files = vec![GraphFile {
rel: "src/A.cs".to_string(),
mtime: 1,
}];
rebuild_graph(&dir, &graph_files, &fresh, true, None, None).unwrap();
assert!(graph_dir(&dir).join("fragments-v44.json").exists());
assert!(graph_dir(&dir).join("fragments-index-v44.json").exists());
for earlier in ["v40", "v41"] {
assert!(
!graph_dir(&dir)
.join(format!("fragments-{earlier}.json"))
.exists(),
"the {earlier} pair is deleted -- rename IS the invalidation"
);
assert!(!graph_dir(&dir)
.join(format!("fragments-index-{earlier}.json"))
.exists());
}
}
#[test]
fn a_planted_earlier_rust_pair_is_never_read_and_is_removed() {
for generation in [
"-v25", "-v26", "-v27", "-v28", "-v29", "-v30", "-v31", "-v32", "-v33", "-v34", "-v35",
"-v36", "-v37", "-v38", "-v39", "-v40", "-v41", "-v42",
] {
let dir = temp_dir(&format!("planted{generation}-rust-pair"));
fs::create_dir_all(graph_dir(&dir)).unwrap();
let planted = serde_json::to_string(&serde_json::json!({
"src/lib.rs": {"mtime": 5, "fragment": {"rs": 1, "items": [
{"name": "stale", "kind": "fn", "path": "stale", "line": 1, "nameLine": 1, "endLine": 1}
]}},
}))
.unwrap();
plant_cache_pair(&dir, &planted, r#"{"src/lib.rs":5}"#, generation);
let mut fresh = HashMap::new();
fresh.insert(
"src/lib.rs".to_string(),
AnyFragment::Rs(crate::extract::RsFragment::default()),
);
let graph_files = vec![GraphFile {
rel: "src/lib.rs".to_string(),
mtime: 5,
}];
let g = match rebuild_graph(&dir, &graph_files, &fresh, true, None, None).unwrap() {
RebuildOutcome::Rebuilt(g) => g,
RebuildOutcome::NotRebuilt => unreachable!("changed is true"),
};
assert!(g.defs.iter().all(|d| d.name != "stale"), "{generation}");
assert!(graph_dir(&dir).join("fragments-v44.json").exists());
assert!(graph_dir(&dir).join("fragments-index-v44.json").exists());
assert!(!graph_dir(&dir)
.join(format!("fragments{generation}.json"))
.exists());
assert!(!graph_dir(&dir)
.join(format!("fragments-index{generation}.json"))
.exists());
}
}
fn planted_def_fragment() -> Fragment {
Fragment {
defs: vec![FragDef {
id: "App.Planted".into(),
name: "Planted".into(),
namespace: "App".into(),
kind: "class".into(),
line: 1,
methods: vec![],
properties: vec![],
fields: vec![],
method_returns: OrderedMap::new(),
extension_methods: vec![],
bases: vec![],
type_params: vec![],
base_generic_args: OrderedMap::new(),
test_methods: vec![],
property_types: OrderedMap::new(),
field_types: OrderedMap::new(),
method_return_args: OrderedMap::new(),
non_public_methods: vec![],
method_arities: OrderedMap::new(),
method_type_arities: OrderedMap::new(),
method_type_param_sites: OrderedMap::new(),
method_params: OrderedMap::new(),
override_methods: vec![],
base_type_args: OrderedMap::new(),
property_message_args: Vec::new(),
array_message_bases: Vec::new(),
delegate_signature: None,
end_line: 1,
}],
usings: vec![],
refs: vec![],
names: vec![],
registrations: vec![],
publishes: vec![],
handler_registrations: vec![],
transport: vec![],
}
}
fn rebuilt_over_a_planted_pair(label: &str, generation: &str) -> (std::path::PathBuf, Graph) {
let dir = temp_dir(label);
fs::create_dir_all(graph_dir(&dir)).unwrap();
let planted = serde_json::to_string(&serde_json::json!({
"src/A.cs": {"mtime": 5, "fragment": planted_def_fragment()},
}))
.unwrap();
plant_cache_pair(&dir, &planted, r#"{"src/A.cs":5}"#, generation);
let fresh_fragment = Fragment {
defs: vec![],
usings: vec![],
refs: vec![],
names: vec![],
registrations: vec![],
publishes: vec![],
handler_registrations: vec![],
transport: vec![],
};
let mut fresh = HashMap::new();
fresh.insert("src/A.cs".to_string(), AnyFragment::Cs(fresh_fragment));
let graph_files = vec![GraphFile {
rel: "src/A.cs".to_string(),
mtime: 5,
}];
let g = match rebuild_graph(&dir, &graph_files, &fresh, true, None, None).unwrap() {
RebuildOutcome::Rebuilt(g) => g,
RebuildOutcome::NotRebuilt => unreachable!("changed is true"),
};
(dir, g)
}
#[test]
fn a_planted_v38_or_v39_pair_is_never_read_and_is_removed() {
for generation in ["v38", "v39"] {
let (dir, g) = rebuilt_over_a_planted_pair(
&format!("planted-{generation}-pair"),
&format!("-{generation}"),
);
assert!(
g.defs.iter().all(|d| d.id != "App.Planted"),
"an earlier generation's entry is never reused, even at a matching mtime"
);
assert!(graph_dir(&dir).join("fragments-v44.json").exists());
assert!(graph_dir(&dir).join("fragments-index-v44.json").exists());
assert!(!graph_dir(&dir)
.join(format!("fragments-{generation}.json"))
.exists());
assert!(!graph_dir(&dir)
.join(format!("fragments-index-{generation}.json"))
.exists());
}
let (_, control) = rebuilt_over_a_planted_pair("planted-v44-pair", "-v44");
assert!(control.defs.iter().any(|d| d.id == "App.Planted"));
}
fn cs_def(id: &str, name: &str) -> serde_json::Value {
serde_json::json!({
"id": id,
"name": name,
"namespace": "App.Bus",
"kind": "class",
"line": 1,
"methods": [],
})
}
fn message_and_consumer() -> Vec<(String, AnyFragment)> {
let consumer = serde_json::json!({
"id": "App.Bus.Handler",
"name": "Handler",
"namespace": "App.Bus",
"kind": "class",
"line": 1,
"methods": [],
"bases": ["IConsumer"],
"baseGenericArgs": {"IConsumer": ["LoanRequested"]},
});
vec![
(
"Bus/Messages.cs".to_string(),
cs_fragment(serde_json::json!({
"defs": [cs_def("App.Bus.LoanRequested", "LoanRequested")],
"usings": [],
"refs": [],
})),
),
(
"Bus/Consumers.cs".to_string(),
cs_fragment(serde_json::json!({
"defs": [consumer],
"usings": [],
"refs": [],
})),
),
]
}
fn publisher_with_publish() -> serde_json::Value {
serde_json::json!({
"defs": [cs_def("App.Bus.Publisher", "Publisher")],
"usings": [],
"refs": [],
"publishes": [{
"verb": "Publish",
"message": "LoanRequested",
"namespace": "App.Bus",
"line": 10,
}],
})
}
fn publisher_without_publishes() -> serde_json::Value {
serde_json::json!({
"defs": [cs_def("App.Bus.Publisher", "Publisher")],
"usings": [],
"refs": [],
})
}
fn cs_fragment(value: serde_json::Value) -> AnyFragment {
serde_json::from_value(value).unwrap()
}
fn plant_cache_pair(dir: &std::path::Path, payload: &str, index: &str, generation: &str) {
let form = crate::graph::cache::PAYLOAD_FORM;
let stamp = |entries: &str| format!(r#"{{"payload":{form},"owner":null,"entries":{entries}}}"#);
fs::write(
graph_dir(dir).join(format!("fragments{generation}.json")),
stamp(payload),
)
.unwrap();
fs::write(
graph_dir(dir).join(format!("fragments-index{generation}.json")),
stamp(index),
)
.unwrap();
}
fn bus_files() -> Vec<GraphFile> {
["Bus/Messages.cs", "Bus/Consumers.cs", "Bus/Publisher.cs"]
.iter()
.map(|rel| GraphFile {
rel: (*rel).to_string(),
mtime: 7,
})
.collect()
}
fn fresh_bus_fragments() -> HashMap<String, AnyFragment> {
let mut fresh: HashMap<String, AnyFragment> = message_and_consumer().into_iter().collect();
fresh.insert(
"Bus/Publisher.cs".to_string(),
cs_fragment(publisher_with_publish()),
);
fresh
}
fn rebuilt(dir: &std::path::Path) -> Graph {
match rebuild_graph(dir, &bus_files(), &fresh_bus_fragments(), true, None, None).unwrap() {
RebuildOutcome::Rebuilt(g) => g,
RebuildOutcome::NotRebuilt => unreachable!("changed is true"),
}
}
fn hop_count(g: &Graph) -> usize {
g.edges
.iter()
.filter(|e| matches!(e, Edge::BusHop { .. }))
.count()
}
#[test]
fn a_pre_bump_fragment_cache_cannot_leave_a_supported_hop_unfound() {
let stale = serde_json::to_string(&serde_json::json!({
"Bus/Publisher.cs": {"mtime": 7, "fragment": publisher_without_publishes()},
}))
.unwrap();
let index = r#"{"Bus/Publisher.cs":7}"#;
let dir = temp_dir("bus-hop-pre-bump-cache");
fs::create_dir_all(graph_dir(&dir)).unwrap();
plant_cache_pair(&dir, &stale, index, "-v21");
let g = rebuilt(&dir);
assert_eq!(
hop_count(&g),
1,
"the publish fact comes from this run's extraction, never from the older generation"
);
assert_eq!(g.stats.edges_by_kind.bus_hop, Some(1));
assert!(!graph_dir(&dir).join("fragments-v21.json").exists());
let reused = temp_dir("bus-hop-current-generation-cache");
fs::create_dir_all(graph_dir(&reused)).unwrap();
plant_cache_pair(&reused, &stale, index, "-v44");
let g = rebuilt(&reused);
assert_eq!(hop_count(&g), 0);
assert_eq!(g.stats.edges_by_kind.bus_hop, None);
}
#[test]
fn a_current_generation_pair_without_this_payload_form_is_never_reused() {
let stale = serde_json::to_string(&serde_json::json!({
"Bus/Publisher.cs": {"mtime": 7, "fragment": publisher_without_publishes()},
}))
.unwrap();
let index = r#"{"Bus/Publisher.cs":7}"#;
let other_form = crate::graph::cache::PAYLOAD_FORM + 1;
for (label, stamp) in [
("no-payload-stamp", String::new()),
("other-payload-form", format!(r#""payload":{other_form},"#)),
] {
let dir = temp_dir(label);
fs::create_dir_all(graph_dir(&dir)).unwrap();
fs::write(
fragments_cache_path(&dir),
format!(r#"{{{stamp}"owner":null,"entries":{stale}}}"#),
)
.unwrap();
fs::write(
fragments_index_path(&dir),
format!(r#"{{{stamp}"owner":null,"entries":{index}}}"#),
)
.unwrap();
assert!(read_fragments_index(&dir).is_empty(), "{label}");
assert!(
crate::graph::cache::read_fragments_cache(&dir).is_empty(),
"{label}"
);
let g = rebuilt(&dir);
assert_eq!(hop_count(&g), 1, "{label}");
let rewritten = fs::read_to_string(fragments_index_path(&dir)).unwrap();
assert!(
rewritten.starts_with(&format!(
r#"{{"payload":{}"#,
crate::graph::cache::PAYLOAD_FORM
)),
"{label}: {rewritten}"
);
}
}
#[test]
fn a_rebuild_with_no_publish_site_writes_no_bus_hop_counter_at_all() {
let dir = temp_dir("bus-hop-counter-absent");
let files: Vec<GraphFile> = message_and_consumer()
.iter()
.map(|(rel, _)| GraphFile {
rel: rel.clone(),
mtime: 7,
})
.collect();
let fresh: HashMap<String, AnyFragment> = message_and_consumer().into_iter().collect();
let g = match rebuild_graph(&dir, &files, &fresh, true, None, None).unwrap() {
RebuildOutcome::Rebuilt(g) => g,
RebuildOutcome::NotRebuilt => unreachable!("changed is true"),
};
assert_eq!(hop_count(&g), 0);
assert_eq!(g.stats.edges_by_kind.bus_hop, None);
let written = fs::read_to_string(graph_dir(&dir).join("graph.json")).unwrap();
assert!(
!written.contains("bus-hop"),
"an absent key, not a zero -- a zero would move every bus-free artifact's bytes"
);
}
fn array_message_and_consumers() -> Vec<(String, AnyFragment)> {
let array_consumer = serde_json::json!({
"id": "App.Bus.ArrayHandler",
"name": "ArrayHandler",
"namespace": "App.Bus",
"kind": "class",
"line": 1,
"methods": [],
"bases": ["IConsumer"],
"baseGenericArgs": {"IConsumer": ["Foo"]},
"arrayMessageBases": ["IConsumer"],
});
let single_consumer = serde_json::json!({
"id": "App.Bus.SingleHandler",
"name": "SingleHandler",
"namespace": "App.Bus",
"kind": "class",
"line": 1,
"methods": [],
"bases": ["IConsumer"],
"baseGenericArgs": {"IConsumer": ["Foo"]},
});
vec![
(
"Bus/Messages.cs".to_string(),
cs_fragment(serde_json::json!({
"defs": [cs_def("App.Bus.Foo", "Foo")],
"usings": [],
"refs": [],
})),
),
(
"Bus/Consumers.cs".to_string(),
cs_fragment(serde_json::json!({
"defs": [array_consumer, single_consumer],
"usings": [],
"refs": [],
})),
),
]
}
fn array_publisher_with_publish() -> serde_json::Value {
serde_json::json!({
"defs": [cs_def("App.Bus.Publisher", "Publisher")],
"usings": [],
"refs": [],
"publishes": [{
"verb": "Publish",
"message": "Foo[]",
"namespace": "App.Bus",
"line": 10,
}],
})
}
fn array_consumers_without_array_message_bases() -> serde_json::Value {
serde_json::json!({
"defs": [
{
"id": "App.Bus.ArrayHandler",
"name": "ArrayHandler",
"namespace": "App.Bus",
"kind": "class",
"line": 1,
"methods": [],
"bases": ["IConsumer"],
"baseGenericArgs": {"IConsumer": ["Foo"]},
},
{
"id": "App.Bus.SingleHandler",
"name": "SingleHandler",
"namespace": "App.Bus",
"kind": "class",
"line": 1,
"methods": [],
"bases": ["IConsumer"],
"baseGenericArgs": {"IConsumer": ["Foo"]},
},
],
"usings": [],
"refs": [],
})
}
fn array_bus_files() -> Vec<GraphFile> {
["Bus/Messages.cs", "Bus/Consumers.cs", "Bus/Publisher.cs"]
.iter()
.map(|rel| GraphFile {
rel: (*rel).to_string(),
mtime: 7,
})
.collect()
}
fn fresh_array_bus_fragments() -> HashMap<String, AnyFragment> {
let mut fresh: HashMap<String, AnyFragment> =
array_message_and_consumers().into_iter().collect();
fresh.insert(
"Bus/Publisher.cs".to_string(),
cs_fragment(array_publisher_with_publish()),
);
fresh
}
fn array_handler_names(g: &Graph) -> Vec<String> {
let mut names: Vec<String> = g
.edges
.iter()
.filter_map(|e| match e {
Edge::BusHop { to, .. } => Some(
to.rsplit_once('.')
.map_or(to.clone(), |(_, t)| t.to_string()),
),
_ => None,
})
.collect();
names.sort();
names
}
#[test]
fn a_warm_v22_cache_is_rebuilt_rather_than_read_without_array_facts() {
let stale = serde_json::to_string(&serde_json::json!({
"Bus/Consumers.cs": {"mtime": 7, "fragment": array_consumers_without_array_message_bases()},
}))
.unwrap();
let index = r#"{"Bus/Consumers.cs":7}"#;
let dir = temp_dir("bus-array-warm-v22-cache");
fs::create_dir_all(graph_dir(&dir)).unwrap();
plant_cache_pair(&dir, &stale, index, "-v22");
let g = match rebuild_graph(
&dir,
&array_bus_files(),
&fresh_array_bus_fragments(),
true,
None,
None,
)
.unwrap()
{
RebuildOutcome::Rebuilt(g) => g,
RebuildOutcome::NotRebuilt => unreachable!("changed is true"),
};
assert_eq!(
array_handler_names(&g),
vec!["ArrayHandler".to_string()],
"a fresh extraction, never the warm v22 payload, is what decides the array \
consumer's own identity"
);
assert!(!graph_dir(&dir).join("fragments-v22.json").exists());
let reused = temp_dir("bus-array-current-generation-cache");
fs::create_dir_all(graph_dir(&reused)).unwrap();
plant_cache_pair(&reused, &stale, index, "-v44");
let g = match rebuild_graph(
&reused,
&array_bus_files(),
&fresh_array_bus_fragments(),
true,
None,
None,
)
.unwrap()
{
RebuildOutcome::Rebuilt(g) => g,
RebuildOutcome::NotRebuilt => unreachable!("changed is true"),
};
assert_eq!(
array_handler_names(&g),
Vec::<String>::new(),
"reusing a degraded entry at the current generation is exactly the loss the rename \
prevents: the array bit is gone, so the array publish's own identity (rank 1) \
matches neither handler's now-plain (rank 0) identity, and the route silently \
disappears rather than reaching ArrayHandler"
);
}