use std::path::Path;
use serde::Deserialize;
use crate::graph::Edge;
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OracleRef {
pub file: String,
pub line: u32,
pub member: String,
pub target: Option<String>,
pub external: bool,
pub ambiguous: bool,
pub unit: String,
}
pub fn read_oracle_refs(text: &str) -> Result<Vec<OracleRef>, String> {
text.lines()
.enumerate()
.filter(|(_, l)| !l.trim().is_empty())
.map(|(i, l)| {
serde_json::from_str(l).map_err(|e| format!("refs.jsonl line {}: {e}", i + 1))
})
.collect()
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OracleUnit {
pub name: String,
pub tfm: String,
pub status: String,
pub diagnostics: u32,
pub files: Vec<String>,
}
pub fn read_oracle_units(text: &str) -> Result<Vec<OracleUnit>, String> {
text.lines()
.enumerate()
.filter(|(_, l)| !l.trim().is_empty())
.map(|(i, l)| {
serde_json::from_str(l).map_err(|e| format!("units.jsonl line {}: {e}", i + 1))
})
.collect()
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FlowtraceFact {
#[serde(rename = "type")]
pub kind: String,
pub file: String,
pub line: u32,
#[serde(default)]
pub class: Option<String>,
#[serde(default)]
pub message: Option<String>,
#[serde(default)]
pub fqn: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
struct FlowtraceDocument {
facts: Vec<FlowtraceFact>,
}
pub fn read_flowtrace_facts(text: &str) -> Result<Vec<FlowtraceFact>, String> {
let doc: FlowtraceDocument =
serde_json::from_str(text).map_err(|e| format!("facts.json: {e}"))?;
Ok(doc.facts)
}
pub fn native_dispatch_implements_edges(root: &Path, rel_path: &str, source: &str) -> Vec<Edge> {
let extraction = crate::extract::extract(source);
let fragment = crate::graph::fragment_from_extraction(&extraction);
let fragments = vec![(rel_path.to_string(), fragment)];
let graph = crate::resolve::resolve_graph(root, &fragments);
graph
.edges
.into_iter()
.filter(|e| matches!(e, Edge::Implements { .. }))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_REFS: &str = "{\"file\":\"src/App/Repo.cs\",\"line\":7,\"member\":\"Load\",\"target\":\"Fixture.Domain.Order\",\"external\":false,\"ambiguous\":false,\"unit\":\"App\"}\n";
#[test]
fn oracle_refs_parse_one_record_per_line() {
let refs = read_oracle_refs(SAMPLE_REFS).unwrap();
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].member, "Load");
assert!(!refs[0].external);
}
#[test]
fn oracle_refs_skip_blank_lines_and_name_a_malformed_one() {
let text = format!("{SAMPLE_REFS}\n{{not json}}\n");
let err = read_oracle_refs(&text).unwrap_err();
assert!(
err.contains("line 3"),
"error must name the bad line: {err}"
);
}
#[test]
fn oracle_units_parse_status_and_diagnostics() {
let text = "{\"name\":\"App\",\"tfm\":\"net9.0\",\"status\":\"ok\",\"diagnostics\":0,\"files\":[\"src/App/Repo.cs\"]}\n";
let units = read_oracle_units(text).unwrap();
assert_eq!(units.len(), 1);
assert_eq!(units[0].status, "ok");
assert_eq!(units[0].diagnostics, 0);
}
#[test]
fn flowtrace_facts_parse_the_flat_facts_array() {
let text = r#"{"schemaVersion":1,"facts":[{"type":"publish","file":"src/Api/Program.cs","line":25,"message":"ParcelDispatchedMessage","fqn":"Courier.Api.Messaging.Messages.ParcelDispatchedMessage"}]}"#;
let facts = read_flowtrace_facts(text).unwrap();
assert_eq!(facts.len(), 1);
assert_eq!(facts[0].kind, "publish");
assert_eq!(facts[0].message.as_deref(), Some("ParcelDispatchedMessage"));
}
#[test]
fn native_dispatch_reads_the_committed_oracle_and_flowtrace_bytes() {
let refs_text =
std::fs::read_to_string("fixtures/csharp-semantic/oracle/refs.jsonl").unwrap();
let refs = read_oracle_refs(&refs_text).unwrap();
assert!(refs.iter().any(|r| r.member == "Load" && !r.external));
let units_text =
std::fs::read_to_string("fixtures/csharp-semantic/oracle/units.jsonl").unwrap();
let units = read_oracle_units(&units_text).unwrap();
assert!(units.iter().any(|u| u.name == "App" && u.status == "ok"));
let facts_text = std::fs::read_to_string("fixtures/csharp-flowtrace/facts.json").unwrap();
let facts = read_flowtrace_facts(&facts_text).unwrap();
assert!(facts.iter().any(|f| f.kind == "publish"));
}
#[test]
fn native_dispatch_reproduces_the_counterexample_from_real_resolver_output() {
let source =
std::fs::read_to_string("fixtures/csharp-truth/src/NativeDispatchCounterexample.cs")
.unwrap();
let edges = native_dispatch_implements_edges(
Path::new(env!("CARGO_MANIFEST_DIR")),
"src/NativeDispatchCounterexample.cs",
&source,
);
assert!(
!edges.is_empty(),
"the two-type-argument generic call on an unrelated class must still \
produce implements edges from today's resolver -- if this now emits \
nothing, the fifth red-baseline row is stale and must be re-reviewed, \
not silently kept"
);
}
}