use super::*;
use std::collections::HashMap;
use std::fs;
mod qualified_receiver;
#[test]
fn rebuild_graph_writes_the_v13_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(), 64, "v1..v32 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![],
};
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-v33.json").exists(),
"the v33 payload cache is what gets written"
);
assert!(
graph_dir(&dir).join("fragments-index-v33.json").exists(),
"and its mtime-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_v33_supersedes_earlier_generations() {
let dir = temp_dir("fragments-cache-v33-paths");
assert_eq!(
fragments_cache_path(&dir),
graph_dir(&dir).join("fragments-v33.json")
);
assert_eq!(
fragments_index_path(&dir),
graph_dir(&dir).join("fragments-index-v33.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"] {
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-v33");
fs::create_dir_all(graph_dir(&dir)).unwrap();
fs::write(graph_dir(&dir).join("fragments-v32.json"), b"{}").unwrap();
fs::write(graph_dir(&dir).join("fragments-index-v32.json"), b"{}").unwrap();
let fragment = Fragment {
defs: vec![],
usings: vec![],
refs: vec![],
names: vec![],
registrations: vec![],
publishes: vec![],
handler_registrations: 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-v33.json").exists());
assert!(graph_dir(&dir).join("fragments-index-v33.json").exists());
assert!(
!graph_dir(&dir).join("fragments-v32.json").exists(),
"the v32 pair is deleted -- rename IS the invalidation"
);
assert!(!graph_dir(&dir).join("fragments-index-v32.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",
] {
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-v33.json").exists());
assert!(graph_dir(&dir).join("fragments-index-v33.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![],
}
}
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![],
};
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_v32_pair_is_never_read_and_is_removed() {
let (dir, g) = rebuilt_over_a_planted_pair("planted-v32-pair", "-v32");
assert!(
g.defs.iter().all(|d| d.id != "App.Planted"),
"the previous generation's entry is never reused, even at a matching mtime"
);
assert!(graph_dir(&dir).join("fragments-v33.json").exists());
assert!(graph_dir(&dir).join("fragments-index-v33.json").exists());
assert!(!graph_dir(&dir).join("fragments-v32.json").exists());
assert!(!graph_dir(&dir).join("fragments-index-v32.json").exists());
let (_, control) = rebuilt_over_a_planted_pair("planted-v33-pair", "-v33");
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) {
fs::write(
graph_dir(dir).join(format!("fragments{generation}.json")),
payload,
)
.unwrap();
fs::write(
graph_dir(dir).join(format!("fragments-index{generation}.json")),
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, "-v33");
let g = rebuilt(&reused);
assert_eq!(hop_count(&g), 0);
assert_eq!(g.stats.edges_by_kind.bus_hop, None);
}
#[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, "-v33");
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"
);
}