-
Notifications
You must be signed in to change notification settings - Fork 612
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. |
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
.