1个不稳定版本
新 0.1.0 | 2024年8月20日 |
---|
#159 在 嵌入式开发
170KB
4.5K SLoC
digital_test_runner
解析并运行hnemann的数字逻辑设计器和电路模拟器中使用的测试。测试给出了数字电路的输入和预期输出的简单描述。这个crate允许重用这些测试来测试同一电路的其他实现,无论是不同的模拟器还是硬件。
使用方法
加载测试的最简单方法是加载一个.dig
文件,然后通过编号或名称加载特定的测试
use digital_test_runner::{dig,TestCase};
let dig_file = dig::File::open(path).unwrap();
let test_case = dig_file.load_test(n).unwrap();
要实际运行测试,我们需要一个实现TestDriver trait的驱动程序。这个trait描述了测试运行器和待测设备之间的通信。一旦我们有了驱动程序,我们可以使用TestCase::run_iter函数来获取测试的行迭代器。由于驱动程序和测试本身在执行过程中都可能会失败,所以每一行都被包裹在Result
中。一旦我们解包行,我们就可以检查它,例如检查所有输出信号是否与预期值匹配。
for row in test_case.run_iter(&mut driver)? {
let row = row?;
for entry in row.failing_outputs() {
println!("{}: {} expected {} but found {}", row.line, entry.signal.name, entry.expected, entry.output);
}
}
实现驱动程序
TestDriver trait有一个必需的方法,write_input_and_read_output
,它接收一个值列表,这些值应写入待测设备的输入信号。驱动程序应该等待输出信号稳定,读取它们并返回一个读取的输出值列表。
每次调用 write_input_and_read_output
时,输出值的列表应始终以相同的顺序给出。这使我们能够在构建迭代器时检测到某些错误,例如测试程序读取丢失的输出值。为此,TestCase::run_iter 构造函数在构建迭代器之前,会写入所有输入的默认值并读取相应的输出。
由于 write_input_and_read_output
执行某些形式的 I/O 操作,它可能会失败。因此,该特质提供了一个相关的错误类型 TestDriver::Error
,它应实现 std::error::Error。
TestDriver 特质还提供了一个名为 write_input
的第二个提供方法,当应向正在测试的设备写入某些输入时会被调用,但测试不关心产生的输出。默认情况下,这是通过调用 write_input_and_read_output
并丢弃输出实现的,但如果读取输出值代价高昂,驱动程序可以实现自己的 write_input
版本以进行优化。
如果目标是将测试翻译成不同的语言,可以在 static_test::Driver 中提供的一个简单驱动程序中提供。此驱动程序不提供任何输出数据,但运行器仍然会提供输入和预期输出的列表。这仅适用于简单的“静态”测试,即不直接读取任何输出信号值的测试。
手动加载测试
除了从 dig
文件中读取测试之外,还可以直接从其源代码构建。然而,dig
文件不仅为我们提供了测试的源代码,还提供了输入和输出信号描述。仅通过解析源代码,我们就可以得到一个 ParsedTestCase。要将此转换为完整的 TestCase,我们需要向 ParsedTestCase::with_signals 方法提供 Signal 列表。下面是一个完整示例的设置示例。
输入和输出
这个包与很多“输入”和“输出”相关。这些词始终与正在测试的设备相关联。因此,输入是从测试运行器写入 DUT 的值,输出是测试运行器从 DUT 读取的值。
值
此包提供几种值类型
- InputValue:写入到 DUT 的值
- OutputValue:从 DUT 读取的值
- ExpectedValue:由测试提供并比较输出值的预期值
这些值被定义为枚举,并且都具有两个共同变体:一个表示实际整数值的 Value(i64)
,以及一个表示高阻抗状态的 Z
。请注意,这与例如Verilog中可用的简单值模型相比更简单,因为组成值的位要么全部高阻抗,要么全部不是高阻抗。
此外,OutputValue 和 ExpectedValue 都有 X
变体。对于期望值,X
表示测试不关心输出值。这样的期望值 始终 会检查与输出值相等。对于输出值 X
表示未知值,如果无法读取值,则驱动程序可以返回它(但如果值永远无法读取,最好将其排除在返回的输出值列表之外)。这样的值将 永远不会 与期望值相等,除非期望值也是 X
。
完整示例
以下是一个完整示例,其中测试从源代码加载,信号是手动定义的,以及一个简单的驱动程序。在这个简单的示例中,驱动程序没有与测试设备通信,而是仅实现逻辑本身。与这个crate一样,此示例使用 miette 进行错误处理。
对于更复杂的示例,包括与测试设备通信的驱动程序,请参阅源代码的 examples/
目录。
use digital_test_runner::{InputEntry, InputValue, OutputEntry, OutputValue, ParsedTestCase, Signal, TestDriver};
// Error type for driver
#[derive(Debug)]
struct Error(&'static str);
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for Error {}
// Implement driver
struct Driver(Signal);
impl TestDriver for Driver {
type Error = Error;
fn write_input_and_read_output(
&mut self,
inputs: &[InputEntry<'_>],
) -> Result<Vec<OutputEntry<'_>>, Self::Error> {
let input = inputs.get(0).ok_or(Error("No input"))?;
let value = input.value.value().ok_or(Error("Unexpected Z"))?;
let value = if value == 0 { 1 } else { 0 };
Ok(vec![OutputEntry {
signal: &self.0,
value: value.into(),
}])
}
}
fn main() -> miette::Result<()> {
let source = r#"
A B
0 1
1 0
"#;
let parsed_test: ParsedTestCase = source.parse()?;
let signals = vec![Signal::input("A", 1, 0), Signal::output("B", 1)];
let testcase = parsed_test.with_signals(signals)?;
let mut driver = Driver(Signal::output("B", 1));
for row in testcase.run_iter(&mut driver)? {
for output in row?.outputs {
assert!(output.check());
}
}
Ok(())
}
与数字的比较
以下是此crate与原始Digital程序在测试用例解释方式上的一些已知差异
program
、memory
和init
语句目前不支持。- 如果测试在表达式中直接引用输出信号值,并且测试设备为该信号输出高阻抗
Z
值,此crate将给出错误。Digital在评估表达式时,将随机分配高或低值给该信号。 - 此crate在评估循环界限的表达式时不太严格。Digital要求在
loop
和repeat
语句中的界限必须是常量,而此crate接受任何表达式。请注意,界限在进入循环时评估一次,而不是在每次迭代时评估。
依赖项
~3.5MB
~37K SLoC