#![allow(dead_code)]
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::atomic::{AtomicU64, Ordering};
use devscout_rs::graph;
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
static COUNTER: AtomicU64 = AtomicU64::new(0);
pub const CALLER_FILE: &str = "src/Caller.cs";
pub const COMPILATION_FINGERPRINT: &str = "1111111111111111111111111111111111111111";
fn temp_dir(prefix: &str) -> PathBuf {
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let dir = env::temp_dir().join(format!("scout-lane-{prefix}-{}-{n}", std::process::id()));
fs::create_dir_all(&dir).expect("create temp dir");
fs::canonicalize(&dir).expect("canonicalize temp dir")
}
fn fixture_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures/csharp-enrichment")
}
fn copy_tree(src: &Path, dst: &Path) {
fs::create_dir_all(dst).expect("create dest dir");
for entry in fs::read_dir(src).expect("read source dir") {
let entry = entry.expect("dir entry");
let name = entry.file_name();
let file_type = entry.file_type().expect("entry file type");
if file_type.is_dir() {
copy_tree(&entry.path(), &dst.join(&name));
} else if file_type.is_file() {
fs::copy(entry.path(), dst.join(&name)).expect("copy fixture file");
}
}
}
fn context_fingerprint(identity: &Value, fingerprint: &str) -> String {
context_fingerprint_of(&[(identity.clone(), fingerprint)])
}
fn context_fingerprint_of(pairs: &[(Value, &str)]) -> String {
let preimage = pairs
.iter()
.map(|(identity, fingerprint)| {
let canonical = serde_json::to_string(identity).expect("identity serializes");
format!("{canonical}\u{1f}{fingerprint}")
})
.collect::<Vec<_>>()
.join("\n");
let digest = Sha256::digest(preimage.as_bytes());
digest.iter().map(|b| format!("{b:02x}")).collect()
}
pub fn with_compilations(mut candidate: Value, compilations: &[(Value, &str, &str)]) -> Value {
let entries: Vec<Value> = compilations
.iter()
.map(|(identity, state, reason)| {
json!({
"identity": identity,
"state": state,
"reason": reason,
"fingerprint": COMPILATION_FINGERPRINT,
"imports": [],
})
})
.collect();
let pairs: Vec<(Value, &str)> = compilations
.iter()
.map(|(identity, _, _)| (identity.clone(), COMPILATION_FINGERPRINT))
.collect();
candidate["context"]["envelope"]["compilations"] = Value::Array(entries);
candidate["context"]["contextFingerprint"] = Value::from(context_fingerprint_of(&pairs));
candidate
}
pub fn sha1_hex(bytes: &[u8]) -> String {
use sha1::Digest as _;
sha1::Sha1::digest(bytes)
.iter()
.map(|b| format!("{b:02x}"))
.collect()
}
pub struct Fixture {
pub repo: PathBuf,
pub home: PathBuf,
}
impl Fixture {
pub fn build(prefix: &str) -> Fixture {
let base = temp_dir(prefix);
let repo = base.join("repo");
let home = base.join("home");
fs::create_dir_all(&repo).expect("create repo dir");
fs::create_dir_all(&home).expect("create home dir");
copy_tree(&fixture_root().join("src"), &repo.join("src"));
let fx = Fixture { repo, home };
fx.git(&["init", "-q"]);
fx.git(&["config", "user.email", "test@example.com"]);
fx.git(&["config", "user.name", "test"]);
fx.git(&["add", "."]);
fx.git(&["commit", "-q", "-m", "fixture commit"]);
let init = fx.run(&["init", "--no-hooks", "--no-map"]);
assert!(init.status.success(), "init failed: {init:?}");
fx
}
pub fn run(&self, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_devscout"))
.args(args)
.current_dir(&self.repo)
.env("HOME", &self.home)
.env("SCOUT_REGISTRY", self.home.join("repos.json"))
.env("SCOUT_CONTENT_DB", self.home.join("content.db"))
.output()
.expect("devscout must run")
}
pub fn git(&self, args: &[&str]) -> Output {
let out = Command::new("git")
.args(args)
.current_dir(&self.repo)
.output()
.expect("git must run");
assert!(out.status.success(), "git {args:?} failed: {out:?}");
out
}
pub fn head(&self) -> String {
let out = self.git(&["rev-parse", "HEAD"]);
String::from_utf8(out.stdout)
.expect("git output is utf-8")
.trim()
.to_string()
}
pub fn commit_unrelated_file(&self) {
fs::write(self.repo.join("notes.txt"), "notes\n").expect("write notes");
self.git(&["add", "notes.txt"]);
self.git(&["commit", "-q", "-m", "notes"]);
}
pub fn map(&self, extra: &[&str]) -> String {
let mut args = vec!["map", "src"];
args.extend_from_slice(extra);
let out = self.run(&args);
assert!(out.status.success(), "map failed: {out:?}");
String::from_utf8(out.stdout).expect("stdout is utf-8")
}
pub fn graph_bytes(&self) -> Vec<u8> {
let path = graph::graph_json_path(&self.repo);
fs::read(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()))
}
pub fn graph(&self) -> Value {
serde_json::from_slice(&self.graph_bytes()).expect("graph.json is valid JSON")
}
pub fn artifact_path(&self) -> PathBuf {
graph::compiler_facts_json_path(&self.repo)
}
pub fn first_load_line(&self) -> u64 {
let g = self.graph();
g["edges"]
.as_array()
.expect("edges array")
.iter()
.filter(|e| {
e["kind"] == "uses-member" && e["from_file"] == CALLER_FILE && e["member"] == "Load"
})
.filter_map(|e| e["from_line"].as_u64())
.min()
.expect("the fixture has an ambiguous Config.Load() call")
}
pub fn import(&self, candidate: &Value) {
let path = self.home.join(format!(
"candidate-{}.json",
COUNTER.fetch_add(1, Ordering::SeqCst)
));
fs::write(&path, serde_json::to_vec(candidate).expect("serializes"))
.expect("write candidate");
let out = self.run(&[
"compiler-facts",
"import",
path.to_str().expect("utf-8 path"),
]);
assert!(out.status.success(), "compiler-facts import: {out:?}");
}
pub fn audit(&self, extra: &[&str]) -> Output {
let refs = self.home.join("empty-refs.jsonl");
fs::write(&refs, "").expect("write empty refs");
let mut args = vec!["audit", "--semantic", refs.to_str().expect("utf-8 path")];
args.extend_from_slice(extra);
self.run(&args)
}
pub fn audit_lane(&self) -> String {
let refs = self.home.join("empty-refs.jsonl");
fs::write(&refs, "").expect("write empty refs");
let out = self.run(&[
"audit",
"--semantic",
refs.to_str().expect("utf-8 path"),
"--json",
]);
assert!(out.status.success(), "audit failed: {out:?}");
let report: Value = serde_json::from_slice(&out.stdout).expect("audit --json is JSON");
report["lane"]
.as_str()
.expect("lane is a string")
.to_string()
}
}
pub fn compilation_identity() -> Value {
json!({
"projectPath": "Fixture.csproj",
"projectName": "Fixture",
"requestedTfm": "net9.0",
"effectiveTfm": "net9.0",
"configuration": "Debug",
"platform": "AnyCPU",
})
}
pub fn confirmed_load_site(line: u64) -> Value {
json!({
"file": CALLER_FILE,
"shape": "call",
"span": {"startLine": line, "startChar": 0, "endLine": line, "endChar": 1},
"name": {"line": line, "char": 0},
"caller": {
"assembly": "Fixture",
"type": "Fixtures.Enrichment.Caller",
"member": "unspecified",
"genericArity": 0,
"overloadSignature": "()->void",
},
"resolution": "confirmed",
"candidateReason": "",
"target": {
"assembly": "Fixture",
"type": "Fixtures.Enrichment.Beta.Config",
"member": "Load",
"genericArity": 0,
"overloadSignature": "()->void",
},
"candidates": [],
"compilation": {"identity": compilation_identity(), "fingerprint": COMPILATION_FINGERPRINT},
"documentContentIdentity": "doc-caller",
"targetDocumentContentIdentities": [],
})
}
pub fn candidate(head_sha: &str, imports: Vec<Value>, sites: Vec<Value>) -> Value {
let identity = compilation_identity();
json!({
"format": graph::COMPILER_FACTS_FORMAT,
"contractVersion": graph::COMPILER_FACTS_CONTRACT_VERSION,
"artifactSchemaVersion": graph::COMPILER_FACTS_ARTIFACT_SCHEMA_VERSION,
"producer": {"name": graph::EXPECTED_PRODUCER_NAME, "engineRevision": graph::EXPECTED_ENGINE_REVISION},
"profile": {"target": "net9.0", "configuration": "Debug", "platform": "AnyCPU"},
"dependencyFingerprint": graph::EXPECTED_DEPENDENCY_FINGERPRINT,
"context": {
"schemaVersion": graph::EXPECTED_CONTEXT_SCHEMA_VERSION,
"contextFingerprint": context_fingerprint(&identity, COMPILATION_FINGERPRINT),
"envelope": {
"compilations": [{
"identity": identity,
"state": "complete",
"reason": "",
"fingerprint": COMPILATION_FINGERPRINT,
"imports": imports,
}],
},
},
"sourceSnapshot": {"headSha": head_sha, "dirty": false, "dirtyDigest": ""},
"capabilities": {"requested": ["occurrences"], "provided": ["occurrences"]},
"completion": {"terminal": true},
"units": {"processed": ["Fixture.csproj"], "missing": []},
"coverage": {"state": "complete"},
"diagnostics": [],
"symbols": [],
"occurrences": {
"spanEncoding": "utf16-code-unit-line1-char0-end-exclusive",
"identityEncoding": "fully-qualified-display-format",
"sites": sites,
},
})
}
pub fn lane_line(stdout: &str) -> Option<&str> {
stdout.lines().nth(1)
}