use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT: AtomicU64 = AtomicU64::new(0);
struct Fixture {
root: PathBuf,
registry: PathBuf,
}
impl Fixture {
fn new() -> Self {
let root = std::env::temp_dir().join(format!(
"devscout-delegate-lambda-{}-{}",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
));
fs::create_dir_all(&root).unwrap();
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures/csharp-delegate-lambda");
for name in ["Options.cs", "Registrar.cs", "Host.cs", "Ledger.cs"] {
fs::copy(source.join(name), root.join(name)).unwrap();
}
let registry = root.join("registry.json");
let fx = Self { root, registry };
fx.ok(&["init", "--no-hooks"]);
fx
}
fn run(&self, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_devscout"))
.current_dir(&self.root)
.env("SCOUT_REGISTRY", &self.registry)
.args(args)
.output()
.unwrap()
}
fn ok(&self, args: &[&str]) -> String {
let out = self.run(args);
assert!(
out.status.success(),
"{args:?}: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout).unwrap()
}
fn graph(&self) -> serde_json::Value {
let text = fs::read_to_string(self.root.join(".scout/graph/graph.json"))
.expect("graph.json must exist after init");
serde_json::from_str(&text).unwrap()
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
fn precise_row(line: usize) -> String {
format!("Host.cs:{line} uses-member ")
}
fn splice_edges(graph: &serde_json::Value) -> Vec<serde_json::Value> {
graph["edges"]
.as_array()
.unwrap()
.iter()
.filter(|e| {
e["kind"] == "uses-member" && e["from_file"] == "Ledger.cs" && e["member"] == "Splice"
})
.cloned()
.collect()
}
#[test]
fn a_lambda_parameter_is_typed_from_the_callee_delegate_parameter() {
let fx = Fixture::new();
let configure = fx.ok(&["refs", "Configure"]);
assert!(
configure.contains(&precise_row(12)),
"Action<Options> types the lambda parameter: {configure}"
);
assert!(
configure.contains(&precise_row(21)),
"a bare call resolves through the enclosing type's own method: {configure}"
);
assert!(
!configure.contains(&precise_row(17)),
"overloads disagreeing on the delegate type bind nothing: {configure}"
);
assert!(
!configure.contains(&precise_row(19)),
"an explicitly generic callee records no slot: {configure}"
);
let enabled = fx.ok(&["refs", "Enabled"]);
assert!(
enabled.contains(&precise_row(13)),
"Func<Options, bool> types the lambda parameter: {enabled}"
);
assert!(
enabled.contains(&precise_row(14)),
"Expression<Func<Options, object>> unwraps once: {enabled}"
);
let bind = fx.ok(&["refs", "Bind"]);
assert!(
bind.contains(&precise_row(15)),
"the second lambda parameter is typed positionally: {bind}"
);
assert!(
bind.contains(&precise_row(20)),
"an extension callee types the lambda after its this parameter: {bind}"
);
let open = fx.ok(&["refs", "Open"]);
assert!(
open.contains(&precise_row(16)),
"an in-graph delegate declaration types the lambda: {open}"
);
let tune = fx.ok(&["refs", "Tune"]);
assert!(
tune.contains(&precise_row(18)),
"overloads agreeing on the delegate type bind: {tune}"
);
}
#[test]
fn a_one_parameter_lambda_never_binds_an_overload_taking_a_two_parameter_delegate() {
let fx = Fixture::new();
let splice_edges = splice_edges(&fx.graph());
let edge_at = |line: u64| splice_edges.iter().find(|e| e["from_line"] == line);
for line in [29, 34] {
let edge = edge_at(line)
.unwrap_or_else(|| panic!("no Splice edge at line {line}: {splice_edges:#?}"));
assert_eq!(
edge["to"], "Fixture.Ext.LedgerExtensions",
"line {line} must bind the one-parameter extension, not the two-parameter instance method: {splice_edges:#?}"
);
assert_eq!(
edge["heuristic"], true,
"line {line} must not be a PRECISE edge to the two-parameter instance method: {splice_edges:#?}"
);
}
let two_param =
edge_at(39).unwrap_or_else(|| panic!("no Splice edge at line 39: {splice_edges:#?}"));
assert_eq!(two_param["to"], "Fixture.Domain.Ledger");
assert!(
two_param["heuristic"].is_null(),
"line 39 must resolve precisely: {splice_edges:#?}"
);
}
#[test]
fn a_local_function_passed_as_a_method_group_never_binds_an_overload_taking_a_two_parameter_delegate(
) {
let fx = Fixture::new();
let splice_edges = splice_edges(&fx.graph());
let edge_at = |line: u64| splice_edges.iter().find(|e| e["from_line"] == line);
let one_param =
edge_at(45).unwrap_or_else(|| panic!("no Splice edge at line 45: {splice_edges:#?}"));
assert_eq!(
one_param["to"], "Fixture.Ext.LedgerExtensions",
"line 45 must bind the one-parameter extension, not the two-parameter instance method: {splice_edges:#?}"
);
assert_eq!(
one_param["heuristic"], true,
"line 45 must not be a PRECISE edge to the two-parameter instance method: {splice_edges:#?}"
);
let two_param =
edge_at(51).unwrap_or_else(|| panic!("no Splice edge at line 51: {splice_edges:#?}"));
assert_eq!(two_param["to"], "Fixture.Domain.Ledger");
assert!(
two_param["heuristic"].is_null(),
"line 51 must resolve precisely: {splice_edges:#?}"
);
}