Skip to content

Commit

Permalink
Add simple API for generating testharnesses inline (#4629)
Browse files Browse the repository at this point in the history
  • Loading branch information
tmckay-sifive authored Feb 18, 2025
1 parent e20069e commit 1904574
Show file tree
Hide file tree
Showing 2 changed files with 274 additions and 0 deletions.
116 changes: 116 additions & 0 deletions src/main/scala/chisel3/experimental/inlinetest/InlineTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// SPDX-License-Identifier: Apache-2.0

package chisel3.experimental.inlinetest

import chisel3._
import chisel3.experimental.hierarchy.{Definition, Instance}

/** Per-test parametrization needed to build a testharness that instantiates
* the DUT and elaborates a test body.
*
* @tparam M the type of the DUT module
* @tparam R the type of the result returned by the test body
*/
class TestParameters[M <: RawModule, R] private[inlinetest] (
/** The [[desiredName]] of the DUT module. */
val dutName: String,
/** The user-provided name of the test. */
val testName: String,
/** A Definition of the DUT module. */
val dutDefinition: Definition[M],
/** The body for this test, returns a result. */
val testBody: Instance[M] => R
) {
final def desiredTestModuleName = s"test_${dutName}_${testName}"
}

/** Contains traits that implement behavior common to generators for unit test testharness modules. */
object TestHarness {
import chisel3.{Module => ChiselModule, RawModule => ChiselRawModule}

/** TestHarnesses for inline tests without clock and reset IOs should extend this. This
* trait sets the correct desiredName for the module, instantiates the DUT, and provides
* methods to elaborate the test.
*
* @tparam M the type of the DUT module
* @tparam R the type of the result returned by the test body
*/
trait RawModule[M <: ChiselRawModule, R] extends Public { this: ChiselRawModule =>
def test: TestParameters[M, R]
override def desiredName = test.desiredTestModuleName
val dut = Instance(test.dutDefinition)
final def elaborateTest(): R = test.testBody(dut)
}

/** TestHarnesses for inline tests should extend this. This trait sets the correct desiredName for
* the module, instantiates the DUT, and provides methods to elaborate the test. By default, the
* reset is synchronous, but this can be changed by overriding [[resetType]].
*
* @tparam M the type of the DUT module
* @tparam R the type of the result returned by the test body
*/
trait Module[M <: ChiselRawModule, R] extends RawModule[M, R] { this: ChiselModule =>
override def resetType = Module.ResetType.Synchronous
}
}

/** An implementation of a testharness generator. This is a type class that defines how to
* generate a testharness. It is passed to each invocation of [[HasTests.test]].
*
* @tparam M the type of the DUT module
* @tparam R the type of the result returned by the test body
*/
trait TestHarnessGenerator[M <: RawModule, R] {

/** Generate a testharness module given the test parameters. */
def generate(test: TestParameters[M, R]): RawModule with Public
}

object TestHarnessGenerator {

/** The minimal implementation of a unit testharness. Has a clock input and a synchronous reset
* input. Connects these to the DUT and does nothing else.
*/
class UnitTestHarness[M <: RawModule](val test: TestParameters[M, Unit])
extends Module
with TestHarness.Module[M, Unit] {
elaborateTest()
}

implicit def unitTestHarness[M <: RawModule]: TestHarnessGenerator[M, Unit] = new TestHarnessGenerator[M, Unit] {
override def generate(test: TestParameters[M, Unit]) = new UnitTestHarness(test)
}
}

/** Provides methods to build unit testharnesses inline after this module is elaborated.
*
* @tparam TestResult the type returned from each test body generator, typically
* hardware indicating completion and/or exit code to the testharness.
*/
trait HasTests[M <: RawModule] { module: M =>

/** A Definition of the DUT to be used for each of the tests. */
private lazy val moduleDefinition =
module.toDefinition.asInstanceOf[Definition[module.type]]

/** Generate an additional parent around this module.
*
* @param parent generator function, should instantiate the [[Definition]]
*/
protected final def elaborateParentModule(parent: Definition[module.type] => RawModule with Public): Unit =
afterModuleBuilt { Definition(parent(moduleDefinition)) }

/** Generate a public module that instantiates this module. The default
* testharness has clock and synchronous reset IOs and contains the test
* body.
*
* @param testBody the circuit to elaborate inside the testharness
*/
protected final def test[R](
testName: String
)(testBody: Instance[M] => R)(implicit th: TestHarnessGenerator[M, R]): Unit =
elaborateParentModule { moduleDefinition =>
val test = new TestParameters[M, R](desiredName, testName, moduleDefinition, testBody)
th.generate(test)
}
}
158 changes: 158 additions & 0 deletions src/test/scala/chiselTests/experimental/InlineTestSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package chiselTests

import chisel3._
import chisel3.util.Enum
import chisel3.testers._
import chisel3.experimental.inlinetest._
import chisel3.experimental.hierarchy._

class TestResultBundle extends Bundle {
val finish = Output(Bool())
val code = Output(UInt(8.W))
}

// Here is a testharness that consumes some kind of hardware from the test body, e.g.
// a finish and pass/fail interface.
object TestHarnessWithResultIO {
class TestHarnessWithResultIOModule[M <: RawModule](val test: TestParameters[M, TestResultBundle])
extends Module
with TestHarness.Module[M, TestResultBundle] {
val result = IO(new TestResultBundle)
result := elaborateTest()
}
implicit def testharnessGenerator[M <: RawModule]: TestHarnessGenerator[M, TestResultBundle] =
new TestHarnessGenerator[M, TestResultBundle] {
def generate(test: TestParameters[M, TestResultBundle]) = new TestHarnessWithResultIOModule(test)
}
}

object TestHarnessWithMonitorSocket {
// Here is a testharness that expects some sort of interface on its DUT, e.g. a probe
// socket to which to attach a monitor.
class TestHarnessWithMonitorSocketModule[M <: RawModule with HasMonitorSocket](val test: TestParameters[M, Unit])
extends Module
with TestHarness.Module[M, Unit] {
val monitor = Module(new ProtocolMonitor(dut.monProbe.cloneType))
monitor.io :#= probe.read(dut.monProbe)
elaborateTest()
}
implicit def testharnessGenerator[M <: RawModule with HasMonitorSocket]: TestHarnessGenerator[M, Unit] =
new TestHarnessGenerator[M, Unit] {
def generate(test: TestParameters[M, Unit]): RawModule with Public = new TestHarnessWithMonitorSocketModule(test)
}
}

@instantiable
trait HasMonitorSocket { this: RawModule =>
protected def makeProbe(bundle: ProtocolBundle): ProtocolBundle = {
val monProbe = IO(probe.Probe(chiselTypeOf(bundle)))
probe.define(monProbe, probe.ProbeValue(bundle))
monProbe
}
@public val monProbe: ProtocolBundle
}

class ProtocolBundle(width: Int) extends Bundle {
val in = Flipped(UInt(width.W))
val out = UInt(width.W)
}

class ProtocolMonitor(bundleType: ProtocolBundle) extends Module {
val io = IO(Input(bundleType))
assert(io.in === io.out, "in === out")
}

@instantiable
class ModuleWithTests(ioWidth: Int = 32) extends Module with HasMonitorSocket with HasTests[ModuleWithTests] {
@public val io = IO(new ProtocolBundle(ioWidth))

override val monProbe = makeProbe(io)

io.out := io.in

test("foo") { instance =>
instance.io.in := 3.U(ioWidth.W)
assert(instance.io.out === 3.U): Unit
}

test("bar") { instance =>
instance.io.in := 5.U(ioWidth.W)
assert(instance.io.out =/= 0.U): Unit
}

{
import TestHarnessWithResultIO._
test("with_result") { instance =>
val result = Wire(new TestResultBundle)
val timer = RegInit(0.U)
timer := timer + 1.U
instance.io.in := 5.U(ioWidth.W)
val outValid = instance.io.out =/= 0.U
when(outValid) {
result.code := 0.U
result.finish := timer > 1000.U
}.otherwise {
result.code := 1.U
result.finish := true.B
}
result
}
}

{
import TestHarnessWithMonitorSocket._
test("with_monitor") { instance =>
instance.io.in := 5.U(ioWidth.W)
assert(instance.io.out =/= 0.U): Unit
}
}
}

class InlineTestSpec extends ChiselFlatSpec with FileCheck {
it should "generate a public module for each test" in {
generateFirrtlAndFileCheck(new ModuleWithTests)(
"""
| CHECK: module ModuleWithTests
| CHECK: output monProbe : Probe<{ in : UInt<32>, out : UInt<32>}>
|
| CHECK: public module test_ModuleWithTests_foo
| CHECK-NEXT: input clock : Clock
| CHECK-NEXT: input reset : UInt<1>
| CHECK: inst dut of ModuleWithTests
|
| CHECK: public module test_ModuleWithTests_bar
| CHECK-NEXT: input clock : Clock
| CHECK-NEXT: input reset : UInt<1>
| CHECK: inst dut of ModuleWithTests
|
| CHECK: public module test_ModuleWithTests_with_result
| CHECK-NEXT: input clock : Clock
| CHECK-NEXT: input reset : UInt<1>
| CHECK-NEXT: output result : { finish : UInt<1>, code : UInt<8>}
| CHECK: inst dut of ModuleWithTests
|
| CHECK: public module test_ModuleWithTests_with_monitor
| CHECK-NEXT: input clock : Clock
| CHECK-NEXT: input reset : UInt<1>
| CHECK: inst dut of ModuleWithTests
| CHECK: inst monitor of ProtocolMonitor
| CHECK-NEXT: connect monitor.clock, clock
| CHECK-NEXT: connect monitor.reset, reset
| CHECK-NEXT: connect monitor.io.out, read(dut.monProbe).out
| CHECK-NEXT: connect monitor.io.in, read(dut.monProbe).in
"""
)
}

it should "compile to verilog" in {
generateSystemVerilogAndFileCheck(new ModuleWithTests)(
"""
| CHECK: module ModuleWithTests
| CHECK: module test_ModuleWithTests_foo
| CHECK: module test_ModuleWithTests_bar
| CHECK: module test_ModuleWithTests_with_result
| CHECK: module test_ModuleWithTests_with_monitor
"""
)
}
}

0 comments on commit 1904574

Please sign in to comment.