1use std::cell::RefCell;
16use std::future::Future;
17use std::pin::Pin;
18use std::rc::Rc;
19
20use rustdv_gpi as gpi;
21use rustdv_sim::combinators::{first2, Either};
22use rustdv_sim::handle::top_module;
23use rustdv_sim::log;
24use rustdv_sim::time::{sim_time_ns, SimDuration};
25use rustdv_sim::triggers::Timer;
26
27pub use rustdv_methodology::TestError;
35
36pub use rustdv_methodology::RustdvCtx;
40
41type TestFn =
42 fn(RustdvCtx) -> Pin<Box<dyn Future<Output = Result<(), TestError>>>>;
43
44pub struct TestRegistration {
46 pub name: &'static str,
47 pub module: &'static str,
48 pub file: &'static str,
49 pub line: u32,
50 pub run: TestFn,
51 pub timeout: Option<(u64, &'static str)>,
53 pub skip: bool,
54 pub expect_fail: bool,
55 pub expect_error: Option<&'static str>,
58}
59
60fn sentinel_shim(_ctx: RustdvCtx) -> Pin<Box<dyn Future<Output = Result<(), TestError>>>> {
66 Box::pin(async { Ok(()) })
67}
68
69#[used]
70#[cfg_attr(not(target_vendor = "apple"), link_section = "rustdv_tests")]
72#[cfg_attr(target_vendor = "apple", link_section = "__DATA,rustdv_tests")]
73static SENTINEL: &TestRegistration = &TestRegistration {
74 name: "__rustdv_sentinel",
75 module: "rustdv_runner",
76 file: file!(),
77 line: line!(),
78 run: sentinel_shim,
79 timeout: None,
80 skip: true,
81 expect_fail: false,
82 expect_error: None,
83};
84
85#[cfg(not(target_vendor = "apple"))]
89extern "C" {
90 static __start_rustdv_tests: u8;
91 static __stop_rustdv_tests: u8;
92}
93
94#[cfg(target_vendor = "apple")]
95extern "C" {
96 #[link_name = "\x01section$start$__DATA$rustdv_tests"]
97 static __start_rustdv_tests: u8;
98 #[link_name = "\x01section$end$__DATA$rustdv_tests"]
99 static __stop_rustdv_tests: u8;
100}
101
102pub fn collect_tests() -> Vec<&'static TestRegistration> {
104 std::hint::black_box(SENTINEL.name);
106 let mut out: Vec<&'static TestRegistration> = Vec::new();
107 unsafe {
108 let start = std::ptr::addr_of!(__start_rustdv_tests) as usize;
109 let stop = std::ptr::addr_of!(__stop_rustdv_tests) as usize;
110 let entry = std::mem::size_of::<&TestRegistration>();
111 let count = (stop - start) / entry;
112 let base = start as *const &'static TestRegistration;
113 for i in 0..count {
114 let reg = *base.add(i);
115 if reg.name != "__rustdv_sentinel" {
116 out.push(reg);
117 }
118 }
119 }
120 out.sort_by_key(|r| (r.file, r.line));
121 out
122}
123
124#[derive(Clone, Debug, PartialEq, Eq)]
129enum Outcome {
130 Pass,
131 Fail { msg: String, kind: Option<&'static str> },
134 Skip,
135}
136
137fn fail(msg: impl Into<String>) -> Outcome {
138 Outcome::Fail { msg: msg.into(), kind: None }
139}
140
141struct TestResult {
142 name: &'static str,
143 outcome: Outcome,
144 sim_ns: f64,
145}
146
147thread_local! {
148 static CURRENT_FAILURE: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));
151}
152
153fn take_background_failure() -> Option<String> {
154 CURRENT_FAILURE.with(|f| f.borrow_mut().take())
155}
156
157fn seed_from_env() -> u64 {
158 std::env::var("RUSTDV_RANDOM_SEED")
159 .ok()
160 .and_then(|s| s.parse().ok())
161 .unwrap_or_else(|| {
162 std::time::SystemTime::now()
163 .duration_since(std::time::UNIX_EPOCH)
164 .map(|d| d.as_secs())
165 .unwrap_or(1)
166 })
167}
168
169async fn run_one(reg: &'static TestRegistration, seed: u64) -> Outcome {
170 rustdv_sim::phase::leave_read_only().await;
179
180 rustdv_methodology::ConfigDb::clear();
186 log::reset_config();
187
188 let dut = match top_module() {
189 Ok(d) => d,
190 Err(e) => return fail(format!("no DUT: {e}")),
191 };
192 let ctx = RustdvCtx::new(reg.name, dut, seed);
195
196 let ex = rustdv_sim::executor::current();
197 let watermark = ex.watermark();
198
199 let body = {
206 let watcher = ctx.clone();
207 let fut = (reg.run)(ctx);
208 async move {
209 let result = fut.await;
210 if watcher.objections().ever_raised() {
211 watcher.all_objections_dropped().await;
212 }
213 result
214 }
215 };
216
217 let handle = ex.spawn_named(body, Some(reg.name));
218
219 let raw = match reg.timeout {
221 Some((n, unit)) => {
222 let d = SimDuration::from_unit(n, unit);
223 match first2(handle, Timer::new(d)).await {
224 Either::First(r) => Some(r),
225 Either::Second(()) => None, }
227 }
228 None => Some(handle.await),
229 };
230
231 ex.cancel_after(watermark);
233
234 let mut outcome = match raw {
235 None => fail(format!(
236 "timeout after {}{}",
237 reg.timeout.unwrap().0,
238 reg.timeout.unwrap().1
239 )),
240 Some(Err(e)) => fail(format!("test task: {e}")),
241 Some(Ok(Err(e))) => Outcome::Fail { msg: e.to_string(), kind: e.kind() },
242 Some(Ok(Ok(()))) => Outcome::Pass,
243 };
244
245 if let Some(bg) = take_background_failure() {
247 if outcome == Outcome::Pass {
248 outcome = fail(bg);
249 }
250 }
251
252 if let Some(expected) = reg.expect_error {
253 outcome = match outcome {
254 Outcome::Pass => fail(format!("expected error '{expected}' but test passed")),
255 Outcome::Fail { msg, kind } if kind == Some(expected) => {
256 let _ = msg;
257 Outcome::Pass
258 }
259 Outcome::Fail { msg, kind } => fail(format!(
260 "expected error '{expected}', got {}: {msg}",
261 kind.unwrap_or("an unclassified failure")
262 )),
263 s => s,
264 };
265 } else if reg.expect_fail {
266 outcome = match outcome {
267 Outcome::Pass => fail("expected failure but test passed"),
268 Outcome::Fail { .. } => Outcome::Pass,
269 s => s,
270 };
271 }
272 outcome
273}
274
275fn apply_testcase_filter(
287 tests: Vec<&'static TestRegistration>,
288) -> Result<Vec<&'static TestRegistration>, String> {
289 let Ok(raw) = std::env::var("RUSTDV_TESTCASE") else { return Ok(tests) };
290 let pats: Vec<String> = raw
291 .split(',')
292 .map(|s| s.trim().to_ascii_lowercase())
293 .filter(|s| !s.is_empty())
294 .collect();
295 if pats.is_empty() {
296 return Ok(tests);
297 }
298 let kept: Vec<_> = tests
299 .into_iter()
300 .filter(|t| {
301 let name = t.name.to_ascii_lowercase();
302 pats.iter().any(|p| name.contains(p))
303 })
304 .collect();
305 if kept.is_empty() {
306 return Err(format!("RUSTDV_TESTCASE={raw} matched no test"));
307 }
308 Ok(kept)
309}
310
311async fn regression() {
312 let tests = match apply_testcase_filter(collect_tests()) {
313 Ok(t) => t,
314 Err(e) => {
315 log::error(&e);
316 println!("REGRESSION: FAIL");
317 gpi::finish();
318 return;
319 }
320 };
321 let seed = seed_from_env();
322 log::info(&format!(
323 "rustdv: found {} test(s), RUSTDV_RANDOM_SEED={seed}",
324 tests.len()
325 ));
326
327 let mut results: Vec<TestResult> = Vec::new();
328 let total = tests.len();
329
330 for (i, reg) in tests.iter().enumerate() {
331 if reg.skip {
332 log::info(&format!("skipping {} ({}/{})", reg.name, i + 1, total));
333 results.push(TestResult { name: reg.name, outcome: Outcome::Skip, sim_ns: 0.0 });
334 continue;
335 }
336 log::info(&format!(
337 "running {} ({}/{}) [{}:{}]",
338 reg.name,
339 i + 1,
340 total,
341 reg.file,
342 reg.line
343 ));
344 let t0 = sim_time_ns();
345 let outcome = run_one(reg, seed.wrapping_add(i as u64)).await;
346 let dt = sim_time_ns() - t0;
347 match &outcome {
348 Outcome::Pass => log::info(&format!("{} PASSED", reg.name)),
349 Outcome::Fail { msg, .. } => log::error(&format!("{} FAILED: {msg}", reg.name)),
350 Outcome::Skip => {}
351 }
352 results.push(TestResult { name: reg.name, outcome, sim_ns: dt });
353 }
354
355 print_summary(&results);
356 write_xunit(&results);
357
358 let failed = results.iter().any(|r| matches!(r.outcome, Outcome::Fail { .. }));
359 println!("REGRESSION: {}", if failed { "FAIL" } else { "PASS" });
360 gpi::finish();
361}
362
363fn print_summary(results: &[TestResult]) {
364 println!("{}", "*".repeat(78));
366 println!("** {:<40} {:>8} {:>14} **", "TEST", "STATUS", "SIM TIME (ns)");
367 println!("{}", "*".repeat(78));
368 for r in results {
369 let status = match &r.outcome {
370 Outcome::Pass => "PASS",
371 Outcome::Fail { .. } => "FAIL",
372 Outcome::Skip => "SKIP",
373 };
374 println!("** {:<40} {:>8} {:>14.2} **", r.name, status, r.sim_ns);
375 }
376 println!("{}", "*".repeat(78));
377}
378
379fn write_xunit(results: &[TestResult]) {
382 let Ok(path) = std::env::var("RUSTDV_RESULTS_XML") else { return };
383 let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
384 let failures = results.iter().filter(|r| matches!(r.outcome, Outcome::Fail { .. })).count();
385 let skipped = results.iter().filter(|r| matches!(r.outcome, Outcome::Skip)).count();
386 xml.push_str(&format!(
387 "<testsuites>\n<testsuite name=\"rustdv\" tests=\"{}\" failures=\"{}\" skipped=\"{}\">\n",
388 results.len(),
389 failures,
390 skipped
391 ));
392 for r in results {
393 xml.push_str(&format!(
394 " <testcase name=\"{}\" time=\"{:.2}\"",
395 r.name, r.sim_ns
396 ));
397 match &r.outcome {
398 Outcome::Pass => xml.push_str("/>\n"),
399 Outcome::Skip => xml.push_str("><skipped/></testcase>\n"),
400 Outcome::Fail { msg: m, .. } => xml.push_str(&format!(
401 "><failure message=\"{}\"/></testcase>\n",
402 m.replace('"', "'").replace('<', "(").replace('>', ")")
403 )),
404 }
405 }
406 xml.push_str("</testsuite>\n</testsuites>\n");
407 if let Err(e) = std::fs::write(&path, xml) {
408 log::warning(&format!("could not write {path}: {e}"));
409 }
410}
411
412pub fn vpi_startup() {
420 let cb = gpi::register_start_of_simulation(Box::new(|| {
421 on_start_of_simulation();
422 }));
423 cb.forget();
424}
425
426fn on_start_of_simulation() {
427 let ex = rustdv_sim::init();
428
429 let flag = CURRENT_FAILURE.with(|f| f.clone());
432 ex.set_failure_sink(Box::new(move |msg| {
433 let mut slot = flag.borrow_mut();
434 if slot.is_none() {
435 *slot = Some(msg.to_string());
436 }
437 }));
438 let flag2 = CURRENT_FAILURE.with(|f| f.clone());
439 gpi::set_panic_sink(Box::new(move |msg| {
440 let mut slot = flag2.borrow_mut();
441 if slot.is_none() {
442 *slot = Some(format!("panic in simulator callback: {msg}"));
443 }
444 }));
445
446 ex.spawn_named(regression(), Some("rustdv_regression"));
447 ex.run_until_idle();
448}