devscout_rs/truth/
producer_reader.rs1use std::path::Path;
17
18use serde::Deserialize;
19
20use crate::graph::Edge;
21
22#[derive(Debug, Clone, Deserialize)]
29#[serde(rename_all = "camelCase")]
30pub struct OracleRef {
31 pub file: String,
33 pub line: u32,
35 pub member: String,
37 pub target: Option<String>,
40 pub external: bool,
42 pub ambiguous: bool,
44 pub unit: String,
46}
47
48pub fn read_oracle_refs(text: &str) -> Result<Vec<OracleRef>, String> {
51 text.lines()
52 .enumerate()
53 .filter(|(_, l)| !l.trim().is_empty())
54 .map(|(i, l)| {
55 serde_json::from_str(l).map_err(|e| format!("refs.jsonl line {}: {e}", i + 1))
56 })
57 .collect()
58}
59
60#[derive(Debug, Clone, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct OracleUnit {
65 pub name: String,
67 pub tfm: String,
69 pub status: String,
71 pub diagnostics: u32,
73 pub files: Vec<String>,
75}
76
77pub fn read_oracle_units(text: &str) -> Result<Vec<OracleUnit>, String> {
79 text.lines()
80 .enumerate()
81 .filter(|(_, l)| !l.trim().is_empty())
82 .map(|(i, l)| {
83 serde_json::from_str(l).map_err(|e| format!("units.jsonl line {}: {e}", i + 1))
84 })
85 .collect()
86}
87
88#[derive(Debug, Clone, Deserialize)]
96#[serde(rename_all = "camelCase")]
97pub struct FlowtraceFact {
98 #[serde(rename = "type")]
102 pub kind: String,
103 pub file: String,
105 pub line: u32,
107 #[serde(default)]
109 pub class: Option<String>,
110 #[serde(default)]
113 pub message: Option<String>,
114 #[serde(default)]
116 pub fqn: Option<String>,
117}
118
119#[derive(Debug, Clone, Deserialize)]
120struct FlowtraceDocument {
121 facts: Vec<FlowtraceFact>,
122}
123
124pub fn read_flowtrace_facts(text: &str) -> Result<Vec<FlowtraceFact>, String> {
127 let doc: FlowtraceDocument =
128 serde_json::from_str(text).map_err(|e| format!("facts.json: {e}"))?;
129 Ok(doc.facts)
130}
131
132pub fn native_dispatch_implements_edges(root: &Path, rel_path: &str, source: &str) -> Vec<Edge> {
142 let extraction = crate::extract::extract(source);
143 let fragment = crate::graph::fragment_from_extraction(&extraction);
144 let fragments = vec![(rel_path.to_string(), fragment)];
145 let graph = crate::resolve::resolve_graph(root, &fragments);
146 graph
147 .edges
148 .into_iter()
149 .filter(|e| matches!(e, Edge::Implements { .. }))
150 .collect()
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 const SAMPLE_REFS: &str = "{\"file\":\"src/App/Repo.cs\",\"line\":7,\"member\":\"Load\",\"target\":\"Fixture.Domain.Order\",\"external\":false,\"ambiguous\":false,\"unit\":\"App\"}\n";
158
159 #[test]
160 fn oracle_refs_parse_one_record_per_line() {
161 let refs = read_oracle_refs(SAMPLE_REFS).unwrap();
162 assert_eq!(refs.len(), 1);
163 assert_eq!(refs[0].member, "Load");
164 assert!(!refs[0].external);
165 }
166
167 #[test]
168 fn oracle_refs_skip_blank_lines_and_name_a_malformed_one() {
169 let text = format!("{SAMPLE_REFS}\n{{not json}}\n");
170 let err = read_oracle_refs(&text).unwrap_err();
171 assert!(
172 err.contains("line 3"),
173 "error must name the bad line: {err}"
174 );
175 }
176
177 #[test]
178 fn oracle_units_parse_status_and_diagnostics() {
179 let text = "{\"name\":\"App\",\"tfm\":\"net9.0\",\"status\":\"ok\",\"diagnostics\":0,\"files\":[\"src/App/Repo.cs\"]}\n";
180 let units = read_oracle_units(text).unwrap();
181 assert_eq!(units.len(), 1);
182 assert_eq!(units[0].status, "ok");
183 assert_eq!(units[0].diagnostics, 0);
184 }
185
186 #[test]
187 fn flowtrace_facts_parse_the_flat_facts_array() {
188 let text = r#"{"schemaVersion":1,"facts":[{"type":"publish","file":"src/Api/Program.cs","line":25,"message":"ParcelDispatchedMessage","fqn":"Courier.Api.Messaging.Messages.ParcelDispatchedMessage"}]}"#;
189 let facts = read_flowtrace_facts(text).unwrap();
190 assert_eq!(facts.len(), 1);
191 assert_eq!(facts[0].kind, "publish");
192 assert_eq!(facts[0].message.as_deref(), Some("ParcelDispatchedMessage"));
193 }
194
195 #[test]
196 fn native_dispatch_reads_the_committed_oracle_and_flowtrace_bytes() {
197 let refs_text =
200 std::fs::read_to_string("fixtures/csharp-semantic/oracle/refs.jsonl").unwrap();
201 let refs = read_oracle_refs(&refs_text).unwrap();
202 assert!(refs.iter().any(|r| r.member == "Load" && !r.external));
203
204 let units_text =
205 std::fs::read_to_string("fixtures/csharp-semantic/oracle/units.jsonl").unwrap();
206 let units = read_oracle_units(&units_text).unwrap();
207 assert!(units.iter().any(|u| u.name == "App" && u.status == "ok"));
208
209 let facts_text = std::fs::read_to_string("fixtures/csharp-flowtrace/facts.json").unwrap();
210 let facts = read_flowtrace_facts(&facts_text).unwrap();
211 assert!(facts.iter().any(|f| f.kind == "publish"));
212 }
213
214 #[test]
215 fn native_dispatch_reproduces_the_counterexample_from_real_resolver_output() {
216 let source =
217 std::fs::read_to_string("fixtures/csharp-truth/src/NativeDispatchCounterexample.cs")
218 .unwrap();
219 let edges = native_dispatch_implements_edges(
220 Path::new(env!("CARGO_MANIFEST_DIR")),
221 "src/NativeDispatchCounterexample.cs",
222 &source,
223 );
224 assert!(
225 !edges.is_empty(),
226 "the two-type-argument generic call on an unrelated class must still \
227 produce implements edges from today's resolver -- if this now emits \
228 nothing, the fifth red-baseline row is stale and must be re-reviewed, \
229 not silently kept"
230 );
231 }
232}