-
Notifications
You must be signed in to change notification settings - Fork 616
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add simple API for generating testharnesses inline (#4629)
- Loading branch information
1 parent
e20069e
commit 1904574
Showing
2 changed files
with
274 additions
and
0 deletions.
There are no files selected for viewing
116 changes: 116 additions & 0 deletions
116
src/main/scala/chisel3/experimental/inlinetest/InlineTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
158
src/test/scala/chiselTests/experimental/InlineTestSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
""" | ||
) | ||
} | ||
} |