use std::cell::RefCell;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use rustdv_gpi as gpi;
use rustdv_sim::combinators::{first2, Either};
use rustdv_sim::handle::top_module;
use rustdv_sim::log;
use rustdv_sim::time::{sim_time_ns, SimDuration};
use rustdv_sim::triggers::Timer;
pub use rustdv_methodology::TestError;
pub use rustdv_methodology::RustdvCtx;
type TestFn =
fn(RustdvCtx) -> Pin<Box<dyn Future<Output = Result<(), TestError>>>>;
pub struct TestRegistration {
pub name: &'static str,
pub module: &'static str,
pub file: &'static str,
pub line: u32,
pub run: TestFn,
pub timeout: Option<(u64, &'static str)>,
pub skip: bool,
pub expect_fail: bool,
pub expect_error: Option<&'static str>,
}
fn sentinel_shim(_ctx: RustdvCtx) -> Pin<Box<dyn Future<Output = Result<(), TestError>>>> {
Box::pin(async { Ok(()) })
}
#[used]
#[cfg_attr(not(target_vendor = "apple"), link_section = "rustdv_tests")]
#[cfg_attr(target_vendor = "apple", link_section = "__DATA,rustdv_tests")]
static SENTINEL: &TestRegistration = &TestRegistration {
name: "__rustdv_sentinel",
module: "rustdv_runner",
file: file!(),
line: line!(),
run: sentinel_shim,
timeout: None,
skip: true,
expect_fail: false,
expect_error: None,
};
#[cfg(not(target_vendor = "apple"))]
extern "C" {
static __start_rustdv_tests: u8;
static __stop_rustdv_tests: u8;
}
#[cfg(target_vendor = "apple")]
extern "C" {
#[link_name = "\x01section$start$__DATA$rustdv_tests"]
static __start_rustdv_tests: u8;
#[link_name = "\x01section$end$__DATA$rustdv_tests"]
static __stop_rustdv_tests: u8;
}
pub fn collect_tests() -> Vec<&'static TestRegistration> {
std::hint::black_box(SENTINEL.name);
let mut out: Vec<&'static TestRegistration> = Vec::new();
unsafe {
let start = std::ptr::addr_of!(__start_rustdv_tests) as usize;
let stop = std::ptr::addr_of!(__stop_rustdv_tests) as usize;
let entry = std::mem::size_of::<&TestRegistration>();
let count = (stop - start) / entry;
let base = start as *const &'static TestRegistration;
for i in 0..count {
let reg = *base.add(i);
if reg.name != "__rustdv_sentinel" {
out.push(reg);
}
}
}
out.sort_by_key(|r| (r.file, r.line));
out
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum Outcome {
Pass,
Fail { msg: String, kind: Option<&'static str> },
Skip,
}
fn fail(msg: impl Into<String>) -> Outcome {
Outcome::Fail { msg: msg.into(), kind: None }
}
struct TestResult {
name: &'static str,
outcome: Outcome,
sim_ns: f64,
}
thread_local! {
static CURRENT_FAILURE: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));
}
fn take_background_failure() -> Option<String> {
CURRENT_FAILURE.with(|f| f.borrow_mut().take())
}
fn seed_from_env() -> u64 {
std::env::var("RUSTDV_RANDOM_SEED")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or_else(|| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(1)
})
}
async fn run_one(reg: &'static TestRegistration, seed: u64) -> Outcome {
rustdv_sim::phase::leave_read_only().await;
rustdv_methodology::ConfigDb::clear();
log::reset_config();
let dut = match top_module() {
Ok(d) => d,
Err(e) => return fail(format!("no DUT: {e}")),
};
let ctx = RustdvCtx::new(reg.name, dut, seed);
let ex = rustdv_sim::executor::current();
let watermark = ex.watermark();
let body = {
let watcher = ctx.clone();
let fut = (reg.run)(ctx);
async move {
let result = fut.await;
if watcher.objections().ever_raised() {
watcher.all_objections_dropped().await;
}
result
}
};
let handle = ex.spawn_named(body, Some(reg.name));
let raw = match reg.timeout {
Some((n, unit)) => {
let d = SimDuration::from_unit(n, unit);
match first2(handle, Timer::new(d)).await {
Either::First(r) => Some(r),
Either::Second(()) => None, }
}
None => Some(handle.await),
};
ex.cancel_after(watermark);
let mut outcome = match raw {
None => fail(format!(
"timeout after {}{}",
reg.timeout.unwrap().0,
reg.timeout.unwrap().1
)),
Some(Err(e)) => fail(format!("test task: {e}")),
Some(Ok(Err(e))) => Outcome::Fail { msg: e.to_string(), kind: e.kind() },
Some(Ok(Ok(()))) => Outcome::Pass,
};
if let Some(bg) = take_background_failure() {
if outcome == Outcome::Pass {
outcome = fail(bg);
}
}
if let Some(expected) = reg.expect_error {
outcome = match outcome {
Outcome::Pass => fail(format!("expected error '{expected}' but test passed")),
Outcome::Fail { msg, kind } if kind == Some(expected) => {
let _ = msg;
Outcome::Pass
}
Outcome::Fail { msg, kind } => fail(format!(
"expected error '{expected}', got {}: {msg}",
kind.unwrap_or("an unclassified failure")
)),
s => s,
};
} else if reg.expect_fail {
outcome = match outcome {
Outcome::Pass => fail("expected failure but test passed"),
Outcome::Fail { .. } => Outcome::Pass,
s => s,
};
}
outcome
}
fn apply_testcase_filter(
tests: Vec<&'static TestRegistration>,
) -> Result<Vec<&'static TestRegistration>, String> {
let Ok(raw) = std::env::var("RUSTDV_TESTCASE") else { return Ok(tests) };
let pats: Vec<String> = raw
.split(',')
.map(|s| s.trim().to_ascii_lowercase())
.filter(|s| !s.is_empty())
.collect();
if pats.is_empty() {
return Ok(tests);
}
let kept: Vec<_> = tests
.into_iter()
.filter(|t| {
let name = t.name.to_ascii_lowercase();
pats.iter().any(|p| name.contains(p))
})
.collect();
if kept.is_empty() {
return Err(format!("RUSTDV_TESTCASE={raw} matched no test"));
}
Ok(kept)
}
async fn regression() {
let tests = match apply_testcase_filter(collect_tests()) {
Ok(t) => t,
Err(e) => {
log::error(&e);
println!("REGRESSION: FAIL");
gpi::finish();
return;
}
};
let seed = seed_from_env();
log::info(&format!(
"rustdv: found {} test(s), RUSTDV_RANDOM_SEED={seed}",
tests.len()
));
let mut results: Vec<TestResult> = Vec::new();
let total = tests.len();
for (i, reg) in tests.iter().enumerate() {
if reg.skip {
log::info(&format!("skipping {} ({}/{})", reg.name, i + 1, total));
results.push(TestResult { name: reg.name, outcome: Outcome::Skip, sim_ns: 0.0 });
continue;
}
log::info(&format!(
"running {} ({}/{}) [{}:{}]",
reg.name,
i + 1,
total,
reg.file,
reg.line
));
let t0 = sim_time_ns();
let outcome = run_one(reg, seed.wrapping_add(i as u64)).await;
let dt = sim_time_ns() - t0;
match &outcome {
Outcome::Pass => log::info(&format!("{} PASSED", reg.name)),
Outcome::Fail { msg, .. } => log::error(&format!("{} FAILED: {msg}", reg.name)),
Outcome::Skip => {}
}
results.push(TestResult { name: reg.name, outcome, sim_ns: dt });
}
print_summary(&results);
write_xunit(&results);
let failed = results.iter().any(|r| matches!(r.outcome, Outcome::Fail { .. }));
println!("REGRESSION: {}", if failed { "FAIL" } else { "PASS" });
gpi::finish();
}
fn print_summary(results: &[TestResult]) {
println!("{}", "*".repeat(78));
println!("** {:<40} {:>8} {:>14} **", "TEST", "STATUS", "SIM TIME (ns)");
println!("{}", "*".repeat(78));
for r in results {
let status = match &r.outcome {
Outcome::Pass => "PASS",
Outcome::Fail { .. } => "FAIL",
Outcome::Skip => "SKIP",
};
println!("** {:<40} {:>8} {:>14.2} **", r.name, status, r.sim_ns);
}
println!("{}", "*".repeat(78));
}
fn write_xunit(results: &[TestResult]) {
let Ok(path) = std::env::var("RUSTDV_RESULTS_XML") else { return };
let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
let failures = results.iter().filter(|r| matches!(r.outcome, Outcome::Fail { .. })).count();
let skipped = results.iter().filter(|r| matches!(r.outcome, Outcome::Skip)).count();
xml.push_str(&format!(
"<testsuites>\n<testsuite name=\"rustdv\" tests=\"{}\" failures=\"{}\" skipped=\"{}\">\n",
results.len(),
failures,
skipped
));
for r in results {
xml.push_str(&format!(
" <testcase name=\"{}\" time=\"{:.2}\"",
r.name, r.sim_ns
));
match &r.outcome {
Outcome::Pass => xml.push_str("/>\n"),
Outcome::Skip => xml.push_str("><skipped/></testcase>\n"),
Outcome::Fail { msg: m, .. } => xml.push_str(&format!(
"><failure message=\"{}\"/></testcase>\n",
m.replace('"', "'").replace('<', "(").replace('>', ")")
)),
}
}
xml.push_str("</testsuite>\n</testsuites>\n");
if let Err(e) = std::fs::write(&path, xml) {
log::warning(&format!("could not write {path}: {e}"));
}
}
pub fn vpi_startup() {
let cb = gpi::register_start_of_simulation(Box::new(|| {
on_start_of_simulation();
}));
cb.forget();
}
fn on_start_of_simulation() {
let ex = rustdv_sim::init();
let flag = CURRENT_FAILURE.with(|f| f.clone());
ex.set_failure_sink(Box::new(move |msg| {
let mut slot = flag.borrow_mut();
if slot.is_none() {
*slot = Some(msg.to_string());
}
}));
let flag2 = CURRENT_FAILURE.with(|f| f.clone());
gpi::set_panic_sink(Box::new(move |msg| {
let mut slot = flag2.borrow_mut();
if slot.is_none() {
*slot = Some(format!("panic in simulator callback: {msg}"));
}
}));
ex.spawn_named(regression(), Some("rustdv_regression"));
ex.run_until_idle();
}