-
Notifications
You must be signed in to change notification settings - Fork 613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add simple API for generating testharnesses inline #4629
base: main
Are you sure you want to change the base?
Add simple API for generating testharnesses inline #4629
Conversation
* | ||
* - A clock port | ||
* - A reset port with this module's reset type, or synchronous if unspecified | ||
* - The [[desiredName]] is "[[this.desiredName]] _ [[name]]". |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* - The [[desiredName]] is "[[this.desiredName]] _ [[name]]". | |
* - The [[desiredName]] is "[[this.desiredName]] _ [[testName]]". |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This took me a while to realize, but this is actually doing a separation of the "test harness" (the body
function) from the actual "test". This three-part factoring of (1) DUT, (2) test harness, and (3) test makes sense to me.
With this, have you thought about how to share test harnesses as opposed to having them implicitly inlined in the test? Put differently, is there a way to make the test harness also a module and thereby get the benefits of using D/I on the test harness as opposed to just the DUT?
/** Provides methods to elaborate additional parents to the circuit. */ | ||
trait ElaboratesParents { module: RawModule => | ||
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]] | ||
*/ | ||
def elaborateParentModule(parent: Definition[module.type] => RawModule with Public): Unit = | ||
afterModuleBuilt { Definition(parent(moduleDefinition)) } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this trait be inlined into its single extension trait, HasTestWithResult
?
Alternatively, this could be flattened in the other direction by making this a method on RawModule
.
Pre-emptively creating inheritance hierarchies is usually an anti-pattern. (Or: more strongly, inheritance hierarchies are usually an anti-pattern _unless they are architected and/or have some theoretical basis like Scala collections or Cats [1].)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah it could be flattened. The use case I had in mind that lead to this separation was that the user might have a component with a bunch of tests, while still wanting to elaborate a one-off parent module that is not the same as the testharnesses for all the tests.
So, I wanted to have an open-ended elaborateParentModule
available to the user for that purpose. But, it felt strange to have that in HasTests
, since it's really a separate feature from unit tests; granted one that we can leverage to implement unit tests.
That's how I got here. That said, I'm not very opinionated on the organization. So, if you think it's better Scala or more Chisel-like to flatten these traits, I will happily do so.
testName: String, | ||
definition: Definition[module.type], | ||
body: Instance[module.type] => TestResult | ||
): RawModule with Public = new Module with Public { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A Module with Public
being immediately cast to a RawModule with Public
seems odd.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The intent here is to allow the implementer to override it with something that generates a RawModule.
Good idea, I will look into that. |
I think this would require the ability to make a Definition of the testharness, with a hole in the middle for the test body. Is this possible with Definition/Instance? I could make the testharness and test-body/DUT sibling modules to accomplish something similar, but I think it's important that the DUT instance and test RTL live underneath the testharness hierarchy. |
I have feedback similar to Schuyler's. I think it's really important that we separate the definitions of TestHarnesses from the body of modules that need to be tested. Related to this, I think the type of the TestHarness (and of the TestResult) should not be set for a given module. It seems likely that a single module would want to have tests that have different testharnesses and results. Really, the type of the result should probably be tied to the type of the TestHarness, I don't think they're independent parameters. I think we can do this with typeclasses, but perhaps we should sit down together to work through it (hard to do in a quick sketch). A second order concern (and non-blocking)--I think it is useful to be able to create little tests inline so no issues there, but it should not be the only way. We need to think about test composition where I could define a testharness, write a test against the testharness and some DUT interface, and then have multiple different modules use that test for themselves. Another non-blocking concern, it should be easy to include or elide tests written this way. I should be able to elaborate a module and not also be required to elaborate all of the tests. That can come later though. |
package chisel3.experimental | ||
|
||
import chisel3._ | ||
import chisel3.experimental.hierarchy.{Definition, Instance} | ||
|
||
package object inlinetest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style note, package objects
should only be used for things that must be in an object
but you want to be accessible within what is otherwise a package. Every top-level definition here is a regular class
, trait
or object
, so there's no reason to use a package object.
Also, the file structure needs to match the package structure.
So basically, please move inlinetest
up to the package
, and put this code in src/main/scala/chisel3/experimental/inlinetest/...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha, thanks. I'm not 100% I did what you asked, but I gave it a shot in 5e5bdd4
783b9d6
to
5e5bdd4
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally this looks great, a few points to consider.
@@ -0,0 +1,80 @@ | |||
package chisel3.experimental.inlinetest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit, the name of this file shouldn't be package.scala
, that is reserved for package objects, e.g. [1].
I'm not sure what to call this file since it has basically everything in the package in it, maybe just InlineTest.scala
(still in directory src/main/scala/chisel3/experimental/inlinetest/
) and we can break it up later if we see fit?
trait TestHarness[M <: RawModule, R] { | ||
|
||
/** Generate a testharness module given the test parameters. */ | ||
def generate(test: TestParameters[M, R]): RawModule with Public | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the type of M
every really used? Or put another way--what is the use case for a TestHarness that only works for a particular kind of DUT Module? I suspect there is one, but maybe it would be good to have an example in the tests to drive the point home.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep I think it's useful to have when you want to reuse a testharness across DUTs where your testharness expects certain interfaces to be present on the DUT. Added an example.
case class TestParameters[M <: RawModule, R]( | ||
/** The [[desiredName]] of the DUT module. */ | ||
dutName: String, | ||
/** The user-provided name of the test. */ | ||
testName: String, | ||
/** A Definition of the DUT module. */ | ||
dutDefinition: Definition[M], | ||
/** The body for this test, returns a result. */ | ||
body: Instance[M] => R | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an annoying detail but one we have to consider as library writers in Chisel.
What are the odds that we are going to have to evolve this type over time, like adding fields? I'm assuming it's high--if so, we should not make this a case class because case classes are nightmares for both source and binary compatibility.
Also, should users be able to create instances of this object or not? Maybe the constructor, factory apply, and copy methods should all be private (we won't have the latter two if we make this not a case class).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the odds that we are going to have to evolve this type over time, like adding fields? I'm assuming it's high--
Yeah, I would expect we will at some point.
if so, we should not make this a case class because case classes are nightmares for both source and binary compatibility.
Good to know. I made it a case class precisely because I wanted to avoid changes to the parameter list of the methods that take them.
I've changed it to a class for now.
* @tparam M the type of the DUT module | ||
* @tparam R the type of the result returned by the test body | ||
*/ | ||
trait TestHarness[M <: RawModule, R] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another nit but something to think about. Neither UnitTestHarness
nor TestHarnessWithResultIO
extend this trait which leads me to believe TestHarness
is the wrong name for this TestHarness-providing-typeclass. I am not sure of the right name though. TestHarnessGenerator
? @seldridge any ideas?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think TestHarnessGenerator
makes more sense. I've changed it to that pending other opinions.
Incidentally, the extra logic I added to the tests now either produces invalid FIR or hits a bug in firtool.
Not sure which yet, need to look into it. |
This reverts commit 3b24f28.
Turns out CIRCT bug. I've uncommented the test so it will continue to fail until fixed. Might need to rewrite the test to circumvent the bug if it's not fixed soon. |
@tmckay-sifive: FYI, this is fixed in CIRCT 1.104.0 and I'm seeing that test do something on my end. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some meandering feedback. I'm primarily looking to see if there is a way to avoid the boilerplate of the test harnesses around resetType, desired name, and the dut instantiation.
I think the main thing to consider is where this is highlighting what we are missing on the FIRRTL side in order to run these tests. We probably want to have a defined "test interface" off of which everything works. This PR isn't incompatible with that in any way.
src/main/scala/chisel3/experimental/inlinetest/InlineTest.scala
Outdated
Show resolved
Hide resolved
src/main/scala/chisel3/experimental/inlinetest/InlineTest.scala
Outdated
Show resolved
Hide resolved
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 body: Instance[M] => R | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is almost a case class. Any reason to not make it one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I originally had it that way, but according to @jackkoenig that is a problem for binary compatibility. I don't fully understand the reason for that, I'll admit.
src/main/scala/chisel3/experimental/inlinetest/InlineTest.scala
Outdated
Show resolved
Hide resolved
src/main/scala/chisel3/experimental/inlinetest/InlineTest.scala
Outdated
Show resolved
Hide resolved
/** 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. | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is clock and reset sufficient? @fabianschuiki has brought up some points about: (1) knowing whether or not the test is done and (2) knowing what the exit status is.
Does this motivate a moderately more heavyweight interface of:
class UnitTestBundle extends Bundle {
val clock = Flipped(Clock())
val reset = Flipped(Bool())
val done = Bool()
val exitCode = UInt(32.W)
}
Alternatively, maybe there are two kinds of tests:
- Tests that finish with either an assertion failure or a stop.
- Tests that finish using the done/exitCode
Note: if the stop
is dropped from (1), then (1) becomes purely a subset of (2).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes great point. This was my main motivation for making it easy for users to override the testharness generator and result from each test. I imagine each project might have their own contract with a test driver to communicate this information. I agree that done
/exitCode
is the obvious way to do that and more or less required for a sensible unit testing setup.
Since I do think most projects will settle on some testharness-testdriver contract to communicate done/pass, think it comes down to whether we prefer to ship an example implementation of this by default, or expect users to implement their own if they want it.
I landed on not adding done/pass because it felt less "presumptuous" of the users simulation environment. But I'm open to arguments that that comes at the cost of usability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason why I'm leaning towards it is that we want to have the ability to do both test-discovery and test-execution for any test defined this way. The part which is missing is that we can do test-discovery with some small changes to FIRRTL to add a simulate
statement/region similar to how we have a formal
statement/region (see: https://github.com/chipsalliance/firrtl-spec/blob/main/spec.md#formal-unit-tests). Essentially, these tests just need a marker to indicate that they are tests and not just modules.
However, while formal
has enough information to tell it also how to run the test (because a formal test is both simpler and the formal construct is powerful enough to capture stimulus via assume
), simulate
does not have enough information to run the test. Even in the simplest formulation of just having clock
/reset
, there are assumptions about how to drive the clock and the reset.
It's kind of like defining an entry point for a program (C/C++'s main function void main(int argc, char ** argv)
(or the equivalent in other languages) comes to mind as does function calling convention).
FWIW, stop
does include an exit code, so perhaps that is actually sufficient to use with only the clock
/reset
formulation. That actually sounds good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense to me. If this is not blocking feedback, I will leave it for now.
I think it is a reasonable expectation that these testharness modules are not the top when we actually compile the simulation--there will be a test-driver layer above that generating clocks and resets, writing waves, etc. So, I think what we are looking for is an ABI between the test-driver and the testharness.
Perhaps testharnesses that can be simulate
d must implement this ABI. At which point, circt-test could have its own intrinsic test-driver that works well with verilator and has sane/configurable logic for clock/reset generation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great! Thank you for driving this!
We will need to sort out how to get the test-discovery and test-execution parts of this working. However, this is a great foundation on top of which we can start. 👍
Contributor Checklist
docs/src
?Type of Improvement
Desired Merge Strategy
Release Notes
Add an API to generate testharnesses inline that are emitted as additional public modules in the output.
Reviewer Checklist (only modified by reviewer)
3.6.x
,5.x
, or6.x
depending on impact, API modification or big change:7.0
)?Enable auto-merge (squash)
, clean up the commit message, and label withPlease Merge
.Create a merge commit
.