1use std::cell::RefCell;
16use std::future::Future;
17use std::pin::Pin;
18use std::rc::Rc;
19
20use linkme::distributed_slice;
21use rustdv_gpi as gpi;
22use rustdv_sim::combinators::{Either, first2};
23use rustdv_sim::handle::top_module;
24use rustdv_sim::log;
25use rustdv_sim::time::{SimDuration, sim_time_ns};
26use rustdv_sim::triggers::Timer;
27
28pub use rustdv_methodology::TestError;
36
37pub use rustdv_methodology::RustdvCtx;
41
42type TestFn = 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
60#[doc(hidden)]
69#[distributed_slice]
70pub static TEST_REGISTRATIONS: [TestRegistration];
71
72pub fn collect_tests() -> Vec<&'static TestRegistration> {
74 let mut out: Vec<&'static TestRegistration> = TEST_REGISTRATIONS.iter().collect();
75 out.sort_by_key(|r| (r.file, r.line));
76 out
77}
78
79#[derive(Clone, Debug, PartialEq, Eq)]
84enum Outcome {
85 Pass,
86 Fail {
89 msg: String,
90 kind: Option<&'static str>,
91 },
92 Skip,
93}
94
95fn fail(msg: impl Into<String>) -> Outcome {
96 Outcome::Fail {
97 msg: msg.into(),
98 kind: None,
99 }
100}
101
102struct TestResult {
103 name: &'static str,
104 outcome: Outcome,
105 sim_ns: f64,
106}
107
108thread_local! {
109 static CURRENT_FAILURE: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));
112}
113
114fn take_background_failure() -> Option<String> {
115 CURRENT_FAILURE.with(|f| f.borrow_mut().take())
116}
117
118fn seed_from_env() -> u64 {
119 std::env::var("RUSTDV_RANDOM_SEED")
120 .ok()
121 .and_then(|s| s.parse().ok())
122 .unwrap_or_else(|| {
123 std::time::SystemTime::now()
124 .duration_since(std::time::UNIX_EPOCH)
125 .map(|d| d.as_secs())
126 .unwrap_or(1)
127 })
128}
129
130async fn run_one(reg: &'static TestRegistration, seed: u64) -> Outcome {
131 rustdv_sim::phase::leave_read_only().await;
140
141 rustdv_methodology::ConfigDb::clear();
147 log::reset_config();
148
149 let dut = match top_module() {
150 Ok(d) => d,
151 Err(e) => return fail(format!("no DUT: {e}")),
152 };
153 let ctx = RustdvCtx::new(reg.name, dut, seed);
156
157 let ex = rustdv_sim::executor::current();
158 let watermark = ex.watermark();
159
160 let body = {
167 let watcher = ctx.clone();
168 let fut = (reg.run)(ctx);
169 async move {
170 let result = fut.await;
171 if watcher.objections().ever_raised() {
172 watcher.all_objections_dropped().await;
173 }
174 result
175 }
176 };
177
178 let handle = ex.spawn_named(body, Some(reg.name));
179
180 let raw = match reg.timeout {
182 Some((n, unit)) => {
183 let d = SimDuration::from_unit(n, unit);
184 match first2(handle, Timer::new(d)).await {
185 Either::First(r) => Some(r),
186 Either::Second(()) => None, }
188 }
189 None => Some(handle.await),
190 };
191
192 ex.cancel_after(watermark);
194
195 let mut outcome = match raw {
196 None => fail(format!(
197 "timeout after {}{}",
198 reg.timeout.unwrap().0,
199 reg.timeout.unwrap().1
200 )),
201 Some(Err(e)) => fail(format!("test task: {e}")),
202 Some(Ok(Err(e))) => Outcome::Fail {
203 msg: e.to_string(),
204 kind: e.kind(),
205 },
206 Some(Ok(Ok(()))) => Outcome::Pass,
207 };
208
209 if let Some(bg) = take_background_failure()
211 && outcome == Outcome::Pass
212 {
213 outcome = fail(bg);
214 }
215
216 if let Some(expected) = reg.expect_error {
217 outcome = match outcome {
218 Outcome::Pass => fail(format!("expected error '{expected}' but test passed")),
219 Outcome::Fail { msg, kind } if kind == Some(expected) => {
220 let _ = msg;
221 Outcome::Pass
222 }
223 Outcome::Fail { msg, kind } => fail(format!(
224 "expected error '{expected}', got {}: {msg}",
225 kind.unwrap_or("an unclassified failure")
226 )),
227 s => s,
228 };
229 } else if reg.expect_fail {
230 outcome = match outcome {
231 Outcome::Pass => fail("expected failure but test passed"),
232 Outcome::Fail { .. } => Outcome::Pass,
233 s => s,
234 };
235 }
236 outcome
237}
238
239fn apply_testcase_filter(
251 tests: Vec<&'static TestRegistration>,
252) -> Result<Vec<&'static TestRegistration>, String> {
253 let Ok(raw) = std::env::var("RUSTDV_TESTCASE") else {
254 return Ok(tests);
255 };
256 let pats: Vec<String> = raw
257 .split(',')
258 .map(|s| s.trim().to_ascii_lowercase())
259 .filter(|s| !s.is_empty())
260 .collect();
261 if pats.is_empty() {
262 return Ok(tests);
263 }
264 let kept: Vec<_> = tests
265 .into_iter()
266 .filter(|t| {
267 let name = t.name.to_ascii_lowercase();
268 pats.iter().any(|p| name.contains(p))
269 })
270 .collect();
271 if kept.is_empty() {
272 return Err(format!("RUSTDV_TESTCASE={raw} matched no test"));
273 }
274 Ok(kept)
275}
276
277async fn regression() {
278 let tests = match apply_testcase_filter(collect_tests()) {
279 Ok(t) => t,
280 Err(e) => {
281 log::error(&e);
282 println!("REGRESSION: FAIL");
283 gpi::finish();
284 return;
285 }
286 };
287 let seed = seed_from_env();
288 log::info(&format!(
289 "rustdv: found {} test(s), RUSTDV_RANDOM_SEED={seed}",
290 tests.len()
291 ));
292
293 let mut results: Vec<TestResult> = Vec::new();
294 let total = tests.len();
295
296 for (i, reg) in tests.iter().enumerate() {
297 if reg.skip {
298 log::info(&format!("skipping {} ({}/{})", reg.name, i + 1, total));
299 results.push(TestResult {
300 name: reg.name,
301 outcome: Outcome::Skip,
302 sim_ns: 0.0,
303 });
304 continue;
305 }
306 log::info(&format!(
307 "running {} ({}/{}) [{}:{}]",
308 reg.name,
309 i + 1,
310 total,
311 reg.file,
312 reg.line
313 ));
314 let t0 = sim_time_ns();
315 let outcome = run_one(reg, seed.wrapping_add(i as u64)).await;
316 let dt = sim_time_ns() - t0;
317 match &outcome {
318 Outcome::Pass => log::info(&format!("{} PASSED", reg.name)),
319 Outcome::Fail { msg, .. } => log::error(&format!("{} FAILED: {msg}", reg.name)),
320 Outcome::Skip => {}
321 }
322 results.push(TestResult {
323 name: reg.name,
324 outcome,
325 sim_ns: dt,
326 });
327 }
328
329 print_summary(&results);
330 write_xunit(&results);
331
332 let failed = results
333 .iter()
334 .any(|r| matches!(r.outcome, Outcome::Fail { .. }));
335 println!("REGRESSION: {}", if failed { "FAIL" } else { "PASS" });
336 gpi::finish();
337}
338
339fn print_summary(results: &[TestResult]) {
340 println!("{}", "*".repeat(78));
342 println!(
343 "** {:<40} {:>8} {:>14} **",
344 "TEST", "STATUS", "SIM TIME (ns)"
345 );
346 println!("{}", "*".repeat(78));
347 for r in results {
348 let status = match &r.outcome {
349 Outcome::Pass => "PASS",
350 Outcome::Fail { .. } => "FAIL",
351 Outcome::Skip => "SKIP",
352 };
353 println!("** {:<40} {:>8} {:>14.2} **", r.name, status, r.sim_ns);
354 }
355 println!("{}", "*".repeat(78));
356}
357
358fn write_xunit(results: &[TestResult]) {
361 let Ok(path) = std::env::var("RUSTDV_RESULTS_XML") else {
362 return;
363 };
364 let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
365 let failures = results
366 .iter()
367 .filter(|r| matches!(r.outcome, Outcome::Fail { .. }))
368 .count();
369 let skipped = results
370 .iter()
371 .filter(|r| matches!(r.outcome, Outcome::Skip))
372 .count();
373 xml.push_str(&format!(
374 "<testsuites>\n<testsuite name=\"rustdv\" tests=\"{}\" failures=\"{}\" skipped=\"{}\">\n",
375 results.len(),
376 failures,
377 skipped
378 ));
379 for r in results {
380 xml.push_str(&format!(
381 " <testcase name=\"{}\" time=\"{:.2}\"",
382 r.name, r.sim_ns
383 ));
384 match &r.outcome {
385 Outcome::Pass => xml.push_str("/>\n"),
386 Outcome::Skip => xml.push_str("><skipped/></testcase>\n"),
387 Outcome::Fail { msg: m, .. } => xml.push_str(&format!(
388 "><failure message=\"{}\"/></testcase>\n",
389 m.replace('"', "'").replace('<', "(").replace('>', ")")
390 )),
391 }
392 }
393 xml.push_str("</testsuite>\n</testsuites>\n");
394 if let Err(e) = std::fs::write(&path, xml) {
395 log::warning(&format!("could not write {path}: {e}"));
396 }
397}
398
399pub fn vpi_startup() {
407 let cb = gpi::register_start_of_simulation(Box::new(|| {
408 on_start_of_simulation();
409 }));
410 cb.forget();
411}
412
413fn on_start_of_simulation() {
414 let ex = rustdv_sim::init();
415
416 let flag = CURRENT_FAILURE.with(|f| f.clone());
419 ex.set_failure_sink(Box::new(move |msg| {
420 let mut slot = flag.borrow_mut();
421 if slot.is_none() {
422 *slot = Some(msg.to_string());
423 }
424 }));
425 let flag2 = CURRENT_FAILURE.with(|f| f.clone());
426 gpi::set_panic_sink(Box::new(move |msg| {
427 let mut slot = flag2.borrow_mut();
428 if slot.is_none() {
429 *slot = Some(format!("panic in simulator callback: {msg}"));
430 }
431 }));
432
433 ex.spawn_named(regression(), Some("rustdv_regression"));
434 ex.run_until_idle();
435}