fayalite::testing: add checked_vcd_output!()
This commit is contained in:
parent
31353862ce
commit
5d68885eaf
5 changed files with 211 additions and 3 deletions
|
|
@ -37,7 +37,7 @@ pub use crate::{
|
|||
value::{SimOnly, SimOnlyValue, SimValue, ToSimValue, ToSimValueWithType},
|
||||
},
|
||||
source_location::SourceLocation,
|
||||
testing::{FormalMode, assert_formal},
|
||||
testing::{FormalMode, assert_formal, checked_vcd_output},
|
||||
ty::{AsMask, CanonicalType, TraceAsString, Type},
|
||||
util::{ConstUsize, GenericConstUsize},
|
||||
wire::Wire,
|
||||
|
|
|
|||
|
|
@ -12,11 +12,13 @@ use crate::{
|
|||
bundle::BundleType,
|
||||
firrtl::ExportOptions,
|
||||
module::Module,
|
||||
util::HashMap,
|
||||
sim::{Simulation, vcd::VcdWriterDecls},
|
||||
util::{HashMap, RcWriter},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt::{self, Write},
|
||||
panic::Location,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
sync::{Mutex, OnceLock},
|
||||
|
|
@ -222,3 +224,190 @@ pub fn assert_formal<M: AsRef<Module<T>>, T: BundleType>(
|
|||
)
|
||||
.expect("testing::assert_formal() failed");
|
||||
}
|
||||
|
||||
pub struct CheckedVcdOutput {
|
||||
writer: Option<RcWriter>,
|
||||
expected_path: PathBuf,
|
||||
expected_contents: Result<String, (Option<PathBuf>, std::io::Error)>,
|
||||
location: &'static Location<'static>,
|
||||
}
|
||||
|
||||
impl CheckedVcdOutput {
|
||||
#[must_use]
|
||||
#[track_caller]
|
||||
pub fn new<T: BundleType>(sim: &mut Simulation<T>, expected_path: PathBuf) -> Self {
|
||||
let writer = RcWriter::default();
|
||||
sim.add_trace_writer(VcdWriterDecls::new(writer.clone()));
|
||||
Self {
|
||||
writer: Some(writer),
|
||||
expected_contents: std::fs::read_to_string(&expected_path).map_err(|e| {
|
||||
eprintln!(
|
||||
"error: failed to read expected VCD from: {}",
|
||||
expected_path.display(),
|
||||
);
|
||||
(std::env::current_dir().ok(), e)
|
||||
}),
|
||||
expected_path,
|
||||
location: Location::caller(),
|
||||
}
|
||||
}
|
||||
#[must_use]
|
||||
#[track_caller]
|
||||
#[doc(hidden)]
|
||||
pub fn __checked_vcd_output_macro_helper<T: BundleType>(
|
||||
sim: &mut Simulation<T>,
|
||||
cargo_manifest_dir: &'static str,
|
||||
path: &'static str,
|
||||
) -> Self {
|
||||
Self::new(sim, Path::new(cargo_manifest_dir).join(path))
|
||||
}
|
||||
pub fn with_vcd_output<R>(&self, f: impl FnOnce(&str) -> R) -> R {
|
||||
let Some(writer) = &self.writer else {
|
||||
unreachable!();
|
||||
};
|
||||
writer.clone().borrow(|output| {
|
||||
let Ok(output) = str::from_utf8(output) else {
|
||||
unreachable!("VcdWriter writes valid UTF-8");
|
||||
};
|
||||
f(output)
|
||||
})
|
||||
}
|
||||
#[track_caller]
|
||||
pub fn finish(mut self) {
|
||||
let Ok(()) = self.finish_impl(|msg| panic!("{msg}"));
|
||||
}
|
||||
fn finish_impl<E>(
|
||||
&mut self,
|
||||
error: impl FnOnce(std::fmt::Arguments<'_>) -> E,
|
||||
) -> Result<(), E> {
|
||||
let Self {
|
||||
writer: Some(writer),
|
||||
expected_path,
|
||||
expected_contents,
|
||||
location,
|
||||
} = self
|
||||
else {
|
||||
// already finished
|
||||
return Ok(());
|
||||
};
|
||||
let Ok(vcd) = String::from_utf8(writer.take()) else {
|
||||
unreachable!("VcdWriter writes valid UTF-8");
|
||||
};
|
||||
let expected_path_d = expected_path.display();
|
||||
if expected_contents
|
||||
.as_ref()
|
||||
.is_ok_and(|expected_contents| *expected_contents == vcd)
|
||||
{
|
||||
// avoid written output from being split from threads interleaving writes to stdout
|
||||
let _stdout = std::io::stderr().lock();
|
||||
// use println to get output captured by tests
|
||||
println!("\n{location}: generated VCD matches the expected VCD in {expected_path_d}");
|
||||
return Ok(());
|
||||
}
|
||||
// avoid written output from being split from threads interleaving writes to stderr
|
||||
let _stderr = std::io::stderr().lock();
|
||||
let error = |msg: std::fmt::Arguments<'_>| {
|
||||
// print msg at both beginning and end so it's easier to find when the vcd is huge
|
||||
Err(error(format_args!(
|
||||
"\n{msg}####### VCD:\n{vcd}\n#######\n{msg}"
|
||||
)))
|
||||
};
|
||||
let error = |msg: std::fmt::Arguments<'_>| match &*expected_contents {
|
||||
Ok(_) => error(format_args!(
|
||||
"{location}: generated VCD doesn't match the expected VCD in {expected_path_d}\n\
|
||||
{msg}",
|
||||
)),
|
||||
Err((Some(current_dir), e)) => error(format_args!(
|
||||
"{location}: generated VCD doesn't match the expected VCD in {expected_path_d}\n\
|
||||
error: failed to read: {e}\n\
|
||||
current dir: {current_dir}\n\
|
||||
{msg}",
|
||||
current_dir = current_dir.display(),
|
||||
)),
|
||||
Err((None, e)) => error(format_args!(
|
||||
"{location}: generated VCD doesn't match the expected VCD in {expected_path_d}\n\
|
||||
error: failed to read: {e}\n\
|
||||
{msg}",
|
||||
)),
|
||||
};
|
||||
const OVERWRITE_VAR_NAME: &str = "OVERWRITE_EXPECTED_VCD";
|
||||
const OVERWRITE_VAR_VALUE: &str = "overwrite";
|
||||
match std::env::var_os(OVERWRITE_VAR_NAME) {
|
||||
Some(v) if v == OVERWRITE_VAR_VALUE => match std::fs::write(&expected_path, &vcd) {
|
||||
Ok(()) => error(format_args!(
|
||||
"warning: since `{OVERWRITE_VAR_NAME}={OVERWRITE_VAR_VALUE}` is set -- writing the generated VCD to {expected_path_d}\n"
|
||||
)),
|
||||
Err(e) => error(format_args!(
|
||||
"error: since `{OVERWRITE_VAR_NAME}={OVERWRITE_VAR_VALUE}` is set -- tried to write the generated VCD to {expected_path_d}\n\
|
||||
error: failed to write: {e}"
|
||||
)),
|
||||
},
|
||||
_ => error(format_args!(
|
||||
"note: rerun the test with the environment variable `{OVERWRITE_VAR_NAME}={OVERWRITE_VAR_VALUE}`\n\
|
||||
to update the expected output to match the generated output.\n"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CheckedVcdOutput {
|
||||
#[track_caller]
|
||||
fn drop(&mut self) {
|
||||
let _ = self.finish_impl(|msg| {
|
||||
if std::thread::panicking() {
|
||||
eprintln!("{msg}"); // use eprintln to get output captured by tests
|
||||
} else {
|
||||
panic!("{msg}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
/// Use in tests to check that [`Simulation`] generates the expected VCD traces, by comparing to a `.vcd` file containing the expected traces.
|
||||
///
|
||||
/// Use like so:
|
||||
/// ```
|
||||
/// # use fayalite::prelude::*;
|
||||
/// #
|
||||
/// # #[hdl_module]
|
||||
/// # fn my_module() {
|
||||
/// # #[hdl]
|
||||
/// # let a: UInt<8> = m.input();
|
||||
/// # #[hdl]
|
||||
/// # let b: UInt<8> = m.output();
|
||||
/// # connect(b, 0u8);
|
||||
/// # #[hdl]
|
||||
/// # if a.cmp_eq(100u8) {
|
||||
/// # connect(b, 42u8);
|
||||
/// # }
|
||||
/// # }
|
||||
/// // inside your #[test] fn my_test():
|
||||
///
|
||||
/// // get the module to simulate:
|
||||
/// let m = my_module();
|
||||
/// // create a simulation of the module:
|
||||
/// let mut sim = Simulation::new(m);
|
||||
/// // set up the expected VCD traces, the given .vcd path is relative to env!("CARGO_MANIFEST_DIR")
|
||||
/// let _checked_vcd_output = checked_vcd_output!(
|
||||
/// &mut sim,
|
||||
/// "tests/expected/my_test.vcd",
|
||||
/// );
|
||||
/// // now run the simulation like normal:
|
||||
/// sim.write(sim.io().a, 0u8);
|
||||
/// assert_eq!(sim.read(sim.io().b).as_int(), 0);
|
||||
/// sim.advance_time(SimDuration::from_micros(1));
|
||||
/// sim.write(sim.io().a, 100u8);
|
||||
/// assert_eq!(sim.read(sim.io().b).as_int(), 42);
|
||||
/// ```
|
||||
macro_rules! checked_vcd_output {
|
||||
($sim:expr, $path_relative_to_manifest_dir:expr $(,)?) => {
|
||||
$crate::testing::CheckedVcdOutput::__checked_vcd_output_macro_helper(
|
||||
$sim,
|
||||
$crate::__std::env!("CARGO_MANIFEST_DIR"),
|
||||
$crate::__std::concat!($path_relative_to_manifest_dir),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
pub use checked_vcd_output;
|
||||
|
|
|
|||
6
crates/fayalite/tests/expected/my_test.md
Normal file
6
crates/fayalite/tests/expected/my_test.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<!--
|
||||
SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
See Notices.txt for copyright information
|
||||
-->
|
||||
|
||||
`my_test.vcd` is used in the doctest of `fayalite::testing::checked_vcd_output`
|
||||
13
crates/fayalite/tests/expected/my_test.vcd
Normal file
13
crates/fayalite/tests/expected/my_test.vcd
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
$timescale 1 ps $end
|
||||
$scope module my_module $end
|
||||
$var wire 8 gAF7X a $end
|
||||
$var wire 8 QS=a/ b $end
|
||||
$upscope $end
|
||||
$enddefinitions $end
|
||||
$dumpvars
|
||||
b0 gAF7X
|
||||
b0 QS=a/
|
||||
$end
|
||||
#1000000
|
||||
b1100100 gAF7X
|
||||
b101010 QS=a/
|
||||
|
|
@ -47,7 +47,7 @@ function main()
|
|||
*/LICENSE.md|*/Notices.txt)
|
||||
# copyright file
|
||||
;;
|
||||
/crates/fayalite/tests/ui/*.stderr|/crates/fayalite/tests/sim/expected/*.vcd|/crates/fayalite/tests/sim/expected/*.txt)
|
||||
/crates/fayalite/tests/ui/*.stderr|/crates/fayalite/tests/expected/*.vcd|/crates/fayalite/tests/sim/expected/*.vcd|/crates/fayalite/tests/sim/expected/*.txt)
|
||||
# file that can't contain copyright header
|
||||
;;
|
||||
/.forgejo/workflows/*.yml|*/.gitignore|*.toml|*/Makefile|*/_CoqProject)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue