#digital #circuit #test-runner #input-output

digital_test_runner

解析并运行hnemann的数字逻辑设计器和电路模拟器中使用的测试

1个不稳定版本

0.1.0 2024年8月20日

#159嵌入式开发

MIT/Apache

170KB
4.5K SLoC

Latest version Documentation Build Status MIT Apache

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 读取的值。

此包提供几种值类型

这些值被定义为枚举,并且都具有两个共同变体:一个表示实际整数值的 Value(i64),以及一个表示高阻抗状态的 Z。请注意,这与例如Verilog中可用的简单值模型相比更简单,因为组成值的位要么全部高阻抗,要么全部不是高阻抗。

此外,OutputValueExpectedValue 都有 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程序在测试用例解释方式上的一些已知差异

  • programmemoryinit 语句目前不支持。
  • 如果测试在表达式中直接引用输出信号值,并且测试设备为该信号输出高阻抗 Z 值,此crate将给出错误。Digital在评估表达式时,将随机分配高或低值给该信号。
  • 此crate在评估循环界限的表达式时不太严格。Digital要求在 looprepeat 语句中的界限必须是常量,而此crate接受任何表达式。请注意,界限在进入循环时评估一次,而不是在每次迭代时评估。

依赖项

~3.5MB
~37K SLoC