use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn compile_fail_graph_validation() {
let t = trybuild::TestCases::new();
let ui_dir = manifest_dir().join("tests/ui");
t.compile_fail(ui_dir.join("circular_dependency.rs"));
t.compile_fail(ui_dir.join("circular_dependency_three_nodes.rs"));
t.compile_fail(ui_dir.join("scope_mismatch.rs"));
t.compile_fail(ui_dir.join("scope_mismatch_multiple.rs"));
t.compile_fail(ui_dir.join("missing_dependency.rs"));
t.compile_fail(ui_dir.join("duplicate_registration.rs"));
}
#[test]
fn compile_fail_missing_constructor() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/missing_constructor.rs");
}
#[test]
fn compile_fail_multiple_constructors() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/multiple_constructors.rs");
}
#[test]
fn compile_fail_unknown_attribute() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/unknown_attribute.rs");
}
#[test]
fn compile_fail_unknown_impl_attribute() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/unknown_impl_attribute.rs");
}
#[test]
fn compile_fail_field_arc_no_inject() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/field_arc_no_inject.rs");
}
#[ignore = "snapshot brittle across toolchain versions — run manually to update"]
#[test]
fn compile_fail_resolve_ctx_direct_call() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/resolve_ctx_direct_call.rs");
}
#[test]
fn compile_fail_lifetime_not_injectable() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/lifetime_not_injectable.rs");
}
#[test]
fn compile_fail_generic_struct_requires_bounds() {
let temp_dir = unique_temp_dir("injectable-generic-bounds");
let src_dir = temp_dir.join("src");
fs::create_dir_all(&src_dir).expect("create temp source dir");
let injectable_path = manifest_dir_string();
let injectable_rs_runtime_path = workspace_path_string("injectable-rs-runtime");
let inventory_path = String::from("0.3");
let async_trait_path = String::from("0.1");
let cargo_toml = format!(
r#"[package]
name = "injectable-ui-generic-bounds"
version = "0.0.0"
edition = "2024"
[dependencies]
injectable-rs = {{ path = "{injectable_path}" }}
injectable-rs-runtime = {{ path = "{injectable_rs_runtime_path}" }}
inventory = "{inventory_path}"
async-trait = "{async_trait_path}"
"#
);
fs::write(temp_dir.join("Cargo.toml"), cargo_toml).expect("write temp Cargo.toml");
fs::write(
src_dir.join("main.rs"),
fs::read_to_string(manifest_dir().join("tests/ui/generic_struct_not_supported.rs"))
.expect("read UI fixture"),
)
.expect("write temp main.rs");
let output = Command::new("cargo")
.arg("check")
.arg("--manifest-path")
.arg(temp_dir.join("Cargo.toml"))
.output()
.expect("run cargo check");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"generic struct without bounds should fail to compile.\nstderr:\n{stderr}"
);
assert!(
stderr.contains("cannot be shared between threads safely"),
"expected Sync-related error in stderr:\n{stderr}"
);
assert!(
stderr.contains("cannot be sent between threads safely"),
"expected Send-related error in stderr:\n{stderr}"
);
assert!(
stderr.contains("consider restricting type parameter `T` with trait `Sync`"),
"expected Sync bound suggestion in stderr:\n{stderr}"
);
assert!(
stderr.contains("consider restricting type parameter `T` with trait `Send`"),
"expected Send bound suggestion in stderr:\n{stderr}"
);
let _ = fs::remove_dir_all(&temp_dir);
}
#[test]
fn compile_pass_valid_graph() {
let t = trybuild::TestCases::new();
let ui_dir = manifest_dir().join("tests/ui");
t.pass(ui_dir.join("valid_graph.rs"));
}
#[test]
fn compile_pass_field_ownership_patterns() {
let t = trybuild::TestCases::new();
let ui_dir = manifest_dir().join("tests/ui");
t.pass(ui_dir.join("field_arc_shared.rs")); t.pass(ui_dir.join("field_owned.rs")); }
#[test]
fn compile_fail_owned_non_clone_singleton() {
let t = trybuild::TestCases::new();
let ui_dir = manifest_dir().join("tests/ui");
t.compile_fail(ui_dir.join("field_owned_non_clone_singleton.rs"));
}
fn manifest_dir() -> PathBuf {
std::env::var("CARGO_MANIFEST_DIR")
.map(|d| d.into())
.unwrap_or_else(|_| PathBuf::from("."))
}
fn manifest_dir_string() -> String {
manifest_dir().to_string_lossy().replace('\\', "/")
}
fn workspace_path_string(name: &str) -> String {
manifest_dir()
.parent()
.expect("crate should be in workspace root")
.join(name)
.to_string_lossy()
.replace('\\', "/")
}
fn unique_temp_dir(prefix: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before epoch")
.as_nanos();
std::env::temp_dir().join(format!("{prefix}-{}-{nanos}", std::process::id()))
}