Skip to content

ChiselSim integration for inline tests #4855

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

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
100 changes: 77 additions & 23 deletions src/main/scala/chisel3/experimental/inlinetest/InlineTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

package chisel3.experimental.inlinetest

import scala.collection.mutable

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

Expand All @@ -12,12 +14,12 @@ import chisel3.experimental.hierarchy.{Definition, Instance}
* @tparam R the type of the result returned by the test body
*/
final class TestParameters[M <: RawModule, R] private[inlinetest] (
/** The [[desiredName]] of the DUT module. */
val dutName: String,
/** The [[name]] of the DUT module. */
private[inlinetest] val dutName: () => String,
/** The user-provided name of the test. */
val testName: String,
/** A Definition of the DUT module. */
val dutDefinition: Definition[M],
private[inlinetest] val dutDefinition: () => Definition[M],
/** The body for this test, returns a result. */
private[inlinetest] val testBody: Instance[M] => R,
/** The reset type of the DUT module. */
Expand All @@ -32,7 +34,7 @@ final class TestParameters[M <: RawModule, R] private[inlinetest] (
}

/** The [[desiredName]] for the testharness module. */
private[inlinetest] final def testHarnessDesiredName = s"test_${dutName}_${testName}"
private[inlinetest] final def testHarnessDesiredName = s"test_${dutName()}_${testName}"
}

sealed class TestResultBundle extends Bundle {
Expand Down Expand Up @@ -73,7 +75,7 @@ abstract class TestHarness[M <: RawModule, R](test: TestParameters[M, R])
io.finish := false.B
io.success := true.B

protected final val dut = Instance(test.dutDefinition)
protected final val dut = Instance(test.dutDefinition())
protected final val testResult = test.testBody(dut)
}

Expand Down Expand Up @@ -121,6 +123,24 @@ object TestHarnessGenerator {
}
}

private final class TestGenerator[M <: RawModule, R](
/** The user-provided name of the test. */
val testName: String,
/** Thunk that returns a [[Definition]] of the DUT */
dutDefinition: () => Definition[M],
/** The (eventually) legalized name for the DUT module */
dutName: () => String,
/** The body for this test, returns a result. */
testBody: Instance[M] => R,
/** The reset type of the DUT module. */
dutResetType: Option[Module.ResetType.Type],
/** The testharness generator. */
testHarnessGenerator: TestHarnessGenerator[M, R]
) {
val params = new TestParameters(dutName, testName, dutDefinition, testBody, dutResetType)
def generate() = testHarnessGenerator.generate(params)
}

/** Provides methods to build unit testharnesses inline after this module is elaborated.
*
* @tparam TestResult the type returned from each test body generator, typically
Expand All @@ -135,18 +155,34 @@ trait HasTests { module: RawModule =>
private val inlineTestIncluder = internal.Builder.captureContext().inlineTestIncluder

private def shouldElaborateTest(testName: String) =
inlineTestIncluder.shouldElaborateTest(module.desiredName, testName)
elaborateTests && inlineTestIncluder.shouldElaborateTest(module.desiredName, testName)

/** A Definition of the DUT to be used for each of the tests. */
private lazy val moduleDefinition =
module.toDefinition.asInstanceOf[Definition[module.type]]
/** This module as a definition. Lazy in order to prevent evaluation unless used by a test. */
private lazy val moduleDefinition = module.toDefinition.asInstanceOf[Definition[M]]

/** 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)) }
/** Generators for inline tests by name. LinkedHashMap preserves test insertion order. */
private val testGenerators = new mutable.LinkedHashMap[String, TestGenerator[M, _]]

/** Get the generators for the currently registered tests for this module and whether they are queued
* for elaboration. */
private def getRegisteredTestGenerators: Seq[(TestGenerator[M, _], Boolean)] =
testGenerators.values.toSeq.map { testGenerator =>
(testGenerator, shouldElaborateTest(testGenerator.params.testName))
}

/** Get the currently registered tests for this module and whether they are queued for elaboration. */
def getRegisteredTests: Seq[(TestParameters[M, _], Boolean)] =
getRegisteredTestGenerators.map { case (testGenerator, shouldElaborate) =>
(testGenerator.params, shouldElaborate)
}

private val elaboratedTests = new mutable.HashMap[String, TestHarness[M, _]]

private[chisel3] def getElaboratedTestModule(testName: String): TestHarness[M, _] =
elaboratedTests(testName)

private[chisel3] def getElaboratedTestModules: Seq[(String, TestHarness[M, _])] =
elaboratedTests.toSeq

/** Generate a public module that instantiates this module. The default
* testharness has clock and synchronous reset IOs and contains the test
Expand All @@ -156,15 +192,33 @@ trait HasTests { module: RawModule =>
*/
protected final def test[R](
testName: String
)(testBody: Instance[M] => R)(implicit th: TestHarnessGenerator[M, R]): Unit =
if (elaborateTests && shouldElaborateTest(testName)) {
elaborateParentModule { moduleDefinition =>
val resetType = module match {
case module: Module => Some(module.resetType)
case _ => None
)(testBody: Instance[M] => R)(implicit testHarnessGenerator: TestHarnessGenerator[M, R]): Unit = {
require(!testGenerators.contains(testName), s"test '${testName}' already declared")
val dutResetType = module match {
case module: Module => Some(module.resetType)
case _ => None
}
val testGenerator =
new TestGenerator(
testName,
() => moduleDefinition,
() => module.name,
testBody,
dutResetType,
testHarnessGenerator
)
testGenerators += testName -> testGenerator
}

afterModuleBuilt {
getRegisteredTestGenerators.foreach { case (testGenerator, shouldElaborate) =>
if (shouldElaborate) {
Definition {
val testHarness = testGenerator.generate()
elaboratedTests += testGenerator.params.testName -> testHarness
testHarness
}
val test = new TestParameters[M, R](desiredName, testName, moduleDefinition, testBody, resetType)
th.generate(test)
}
}
}
}
70 changes: 63 additions & 7 deletions src/main/scala/chisel3/simulator/Simulator.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package chisel3.simulator

import chisel3.{Data, RawModule}
import chisel3.experimental.inlinetest.{HasTests, TestHarness}
import firrtl.options.StageUtils.dramaticMessage
import java.nio.file.Paths
import java.nio.file.{FileSystems, PathMatcher, Paths}
import scala.util.{Failure, Success, Try}
import scala.util.control.NoStackTrace
import svsim._
Expand All @@ -27,6 +28,14 @@ object Exceptions {
)
with NoStackTrace

class TestFailed private[simulator]
extends RuntimeException(
dramaticMessage(
header = Some(s"The test finished and signaled failure"),
body = ""
)
)
with NoStackTrace
}

final object Simulator {
Expand Down Expand Up @@ -63,8 +72,9 @@ trait Simulator[T <: Backend] {
*
* @return None if no failures found or Some if they are
*/
private def postProcessLog: Option[Throwable] = {
val log = Paths.get(workspacePath, s"workdir-${tag}", "simulation-log.txt").toFile
private def postProcessLog(workspace: Workspace): Option[Throwable] = {
val log =
Paths.get(workspace.absolutePath, s"${workspace.workingDirectoryPrefix}-${tag}", "simulation-log.txt").toFile
val lines = scala.io.Source
.fromFile(log)
.getLines()
Expand Down Expand Up @@ -116,13 +126,59 @@ trait Simulator[T <: Backend] {
val workspace = new Workspace(path = workspacePath, workingDirectoryPrefix = workingDirectoryPrefix)
workspace.reset()
val elaboratedModule =
workspace.elaborateGeneratedModule(
workspace
.elaborateGeneratedModule(
() => module,
args = chiselOptsModifications(chiselOpts).toSeq,
firtoolArgs = firtoolOptsModifications(firtoolOpts).toSeq
)
workspace.generateAdditionalSources()
_simulate(workspace, elaboratedModule, settings)(body)
}

final def simulateTests[T <: RawModule with HasTests, U](
module: => T,
includeTestGlobs: Seq[String],
chiselOpts: Array[String] = Array.empty,
firtoolOpts: Array[String] = Array.empty,
settings: Settings[TestHarness[T, _]] = Settings.defaultRaw[TestHarness[T, _]]
)(body: (SimulatedModule[TestHarness[T, _]]) => U)(
implicit chiselOptsModifications: ChiselOptionsModifications,
firtoolOptsModifications: FirtoolOptionsModifications,
commonSettingsModifications: svsim.CommonSettingsModifications,
backendSettingsModifications: svsim.BackendSettingsModifications
): Seq[(String, Simulator.BackendInvocationDigest[U])] = {
val workspace = new Workspace(path = workspacePath, workingDirectoryPrefix = workingDirectoryPrefix)
workspace.reset()
val filesystem = FileSystems.getDefault()
workspace
.elaborateAndMakeTestHarnessWorkspaces(
() => module,
includeTestGlobs = includeTestGlobs,
args = chiselOptsModifications(chiselOpts).toSeq,
firtoolArgs = firtoolOptsModifications(firtoolOpts).toSeq
)
workspace.generateAdditionalSources()
.flatMap { case (testWorkspace, testName, elaboratedModule) =>
val includeTest = includeTestGlobs.map { glob =>
filesystem.getPathMatcher(s"glob:$glob")
}.exists(_.matches(Paths.get(testName)))
Option.when(includeTest) {
testWorkspace.generateAdditionalSources()
testName -> _simulate(testWorkspace, elaboratedModule, settings)(body)
}
}
}

private def _simulate[T <: RawModule, U](
workspace: Workspace,
elaboratedModule: ElaboratedModule[T],
settings: Settings[T] = Settings.defaultRaw[T]
)(body: (SimulatedModule[T]) => U)(
implicit chiselOptsModifications: ChiselOptionsModifications,
firtoolOptsModifications: FirtoolOptionsModifications,
commonSettingsModifications: svsim.CommonSettingsModifications,
backendSettingsModifications: svsim.BackendSettingsModifications
): Simulator.BackendInvocationDigest[U] = {
val commonCompilationSettingsUpdated = commonSettingsModifications(
commonCompilationSettings.copy(
// Append to the include directorires based on what the
Expand Down Expand Up @@ -190,13 +246,13 @@ trait Simulator[T <: Backend] {
}
}.transform(
s /*success*/ = { case success =>
postProcessLog match {
postProcessLog(workspace) match {
case None => Success(success)
case Some(error) => Failure(error)
}
},
f /*failure*/ = { case originalError =>
postProcessLog match {
postProcessLog(workspace) match {
case None => Failure(originalError)
case Some(newError) => Failure(newError)
}
Expand Down
Loading
Loading