use assert_cmd::Command;
use predicates::str::contains;
use std::path::Path;
use tempfile::TempDir;
const CSHARP_FIXTURE: &str = "tests/fixtures/csharp-simple";
fn copy_dir_all(src: &Path, dest: &Path) {
std::fs::create_dir_all(dest).unwrap();
for entry in std::fs::read_dir(src).unwrap() {
let entry = entry.unwrap();
let src_path = entry.path();
let dest_path = dest.join(entry.file_name());
if src_path.is_dir() {
copy_dir_all(&src_path, &dest_path);
} else {
std::fs::copy(&src_path, &dest_path).unwrap();
}
}
}
fn setup_csharp_fixture() -> TempDir {
let dir = TempDir::new().unwrap();
let fixture = Path::new(CSHARP_FIXTURE);
copy_dir_all(fixture, dir.path());
dir
}
fn sc_init(dir: &Path) -> assert_cmd::assert::Assert {
Command::cargo_bin("scope")
.unwrap()
.arg("init")
.current_dir(dir)
.assert()
}
fn sc_index_full(dir: &Path) -> assert_cmd::assert::Assert {
Command::cargo_bin("scope")
.unwrap()
.args(["index", "--full"])
.current_dir(dir)
.assert()
}
fn indexed_csharp_db() -> (rusqlite::Connection, TempDir) {
let dir = setup_csharp_fixture();
sc_init(dir.path()).success();
sc_index_full(dir.path()).success();
let db_path = dir.path().join(".scope").join("graph.db");
let conn = rusqlite::Connection::open(&db_path).unwrap();
(conn, dir)
}
#[test]
fn test_init_detects_csharp_from_csproj() {
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join("MyProject.csproj"),
"<Project Sdk=\"Microsoft.NET.Sdk\"></Project>",
)
.unwrap();
sc_init(dir.path()).success().stdout(contains("C#"));
}
#[test]
fn test_index_full_on_csharp_fixture() {
let dir = setup_csharp_fixture();
sc_init(dir.path()).success();
sc_index_full(dir.path())
.success()
.stderr(contains("files"))
.stderr(contains("symbols"));
let graph_db = dir.path().join(".scope").join("graph.db");
assert!(graph_db.exists(), "graph.db should exist after indexing");
assert!(
graph_db.metadata().unwrap().len() > 0,
"graph.db should not be empty"
);
}
#[test]
fn test_index_detects_csharp_classes() {
let (conn, _dir) = indexed_csharp_db();
let symbol_exists = |name: &str, kind: &str| -> bool {
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM symbols WHERE name = ?1 AND kind = ?2",
rusqlite::params![name, kind],
|row| row.get(0),
)
.unwrap();
count > 0
};
assert!(
symbol_exists("PaymentService", "class"),
"PaymentService class should be indexed"
);
assert!(
symbol_exists("OrderController", "class"),
"OrderController class should be indexed"
);
assert!(
symbol_exists("Logger", "class"),
"Logger class should be indexed"
);
}
#[test]
fn test_index_detects_csharp_interfaces() {
let (conn, _dir) = indexed_csharp_db();
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM symbols WHERE name = 'IPaymentService' AND kind = 'interface'",
[],
|row| row.get(0),
)
.unwrap();
assert!(count > 0, "IPaymentService interface should be indexed");
}
#[test]
fn test_index_detects_csharp_enums() {
let (conn, _dir) = indexed_csharp_db();
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM symbols WHERE name = 'PaymentStatus' AND kind = 'enum'",
[],
|row| row.get(0),
)
.unwrap();
assert!(count > 0, "PaymentStatus enum should be indexed");
}
#[test]
fn test_index_detects_csharp_methods() {
let (conn, _dir) = indexed_csharp_db();
let symbol_exists = |name: &str, kind: &str| -> bool {
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM symbols WHERE name = ?1 AND kind = ?2",
rusqlite::params![name, kind],
|row| row.get(0),
)
.unwrap();
count > 0
};
assert!(
symbol_exists("ProcessPayment", "method"),
"ProcessPayment method should be indexed"
);
assert!(
symbol_exists("RefundPayment", "method"),
"RefundPayment method should be indexed"
);
assert!(
symbol_exists("ValidateAmount", "method"),
"ValidateAmount method should be indexed"
);
}
#[test]
fn test_index_detects_csharp_enum_variants() {
let (conn, _dir) = indexed_csharp_db();
let variants: Vec<String> = {
let mut stmt = conn
.prepare("SELECT name FROM symbols WHERE kind = 'variant' ORDER BY name")
.unwrap();
stmt.query_map([], |row| row.get(0))
.unwrap()
.filter_map(|r| r.ok())
.collect()
};
assert!(
variants.contains(&"Pending".to_string()),
"Pending variant should be indexed; found: {variants:?}"
);
assert!(
variants.contains(&"Completed".to_string()),
"Completed variant should be indexed; found: {variants:?}"
);
assert!(
variants.contains(&"Failed".to_string()),
"Failed variant should be indexed; found: {variants:?}"
);
}
#[test]
fn test_csharp_this_method_call_edge_detected() {
let (conn, _dir) = indexed_csharp_db();
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM edges
WHERE (to_id = 'ValidateAmount' OR to_id LIKE '%::ValidateAmount') AND kind = 'calls'",
[],
|row| row.get(0),
)
.unwrap();
assert!(
count > 0,
"this.ValidateAmount() call should generate a 'calls' edge with to_id='ValidateAmount'; got {count}"
);
}
#[test]
fn test_csharp_base_method_call_edge_detected() {
let (conn, _dir) = indexed_csharp_db();
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM edges WHERE kind = 'calls'",
[],
|row| row.get(0),
)
.unwrap();
assert!(
count > 0,
"C# fixture should have 'calls' edges (including base.Method() calls); got {count}"
);
let base_call_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM edges e
JOIN symbols s ON e.to_id = s.id
WHERE s.name = 'OnValidating' AND e.kind = 'calls'",
[],
|row| row.get(0),
)
.unwrap_or(0);
let _ = base_call_count;
assert!(
count > 0,
"base.Method() calls should contribute to overall 'calls' edge count; got {count}"
);
}
#[test]
fn test_csharp_switch_case_variant_ref_edge_detected() {
let (conn, _dir) = indexed_csharp_db();
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM edges WHERE kind = 'references'",
[],
|row| row.get(0),
)
.unwrap();
assert!(
count > 0,
"C# switch case patterns should generate 'references' edges; got {count}"
);
let pending_ref: i64 = conn
.query_row(
"SELECT COUNT(*) FROM edges WHERE kind = 'references' AND to_id = 'Pending'",
[],
|row| row.get(0),
)
.unwrap();
assert!(
pending_ref > 0,
"switch case `case PaymentStatus.Pending:` should generate a 'references' edge with to_id='Pending'; got {pending_ref}"
);
}
#[test]
fn test_sketch_csharp_class() {
let dir = setup_csharp_fixture();
sc_init(dir.path()).success();
sc_index_full(dir.path()).success();
Command::cargo_bin("scope")
.unwrap()
.args(["sketch", "PaymentService"])
.current_dir(dir.path())
.assert()
.success()
.stdout(contains("PaymentService"));
}
#[test]
fn test_sketch_csharp_class_json() {
let dir = setup_csharp_fixture();
sc_init(dir.path()).success();
sc_index_full(dir.path()).success();
let output = Command::cargo_bin("scope")
.unwrap()
.args(["sketch", "PaymentService", "--json"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("Output should be valid JSON");
assert_eq!(json["command"], "sketch");
}
#[test]
fn test_refs_finds_csharp_callers() {
let dir = setup_csharp_fixture();
sc_init(dir.path()).success();
sc_index_full(dir.path()).success();
Command::cargo_bin("scope")
.unwrap()
.args(["refs", "ProcessPayment"])
.current_dir(dir.path())
.assert()
.success();
}