use std::cell::RefCell;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use linkme::distributed_slice;
use rustdv_gpi as gpi;
use rustdv_sim::combinators::{Either, first2};
use rustdv_sim::handle::top_module;
use rustdv_sim::log;
use rustdv_sim::time::{SimDuration, sim_time_ns};
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>,
}
#[doc(hidden)]
#[distributed_slice]
pub static TEST_REGISTRATIONS: [TestRegistration];
pub fn collect_tests() -> Vec<&'static TestRegistration> {
let mut out: Vec<&'static TestRegistration> = TEST_REGISTRATIONS.iter().collect();
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()
&& 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();
}