diff --git a/src/main/scala/chisel3/experimental/inlinetest/InlineTest.scala b/src/main/scala/chisel3/experimental/inlinetest/InlineTest.scala index 953933490d5..a952ef3270f 100644 --- a/src/main/scala/chisel3/experimental/inlinetest/InlineTest.scala +++ b/src/main/scala/chisel3/experimental/inlinetest/InlineTest.scala @@ -2,6 +2,8 @@ package chisel3.experimental.inlinetest +import scala.collection.mutable + import chisel3._ import chisel3.experimental.hierarchy.{Definition, Instance} @@ -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. */ @@ -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 { @@ -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) } @@ -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 @@ -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 @@ -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) } } + } } diff --git a/src/main/scala/chisel3/simulator/Simulator.scala b/src/main/scala/chisel3/simulator/Simulator.scala index fc9235de2fc..0d2f5c4b442 100644 --- a/src/main/scala/chisel3/simulator/Simulator.scala +++ b/src/main/scala/chisel3/simulator/Simulator.scala @@ -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._ @@ -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 { @@ -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() @@ -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 @@ -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) } diff --git a/src/main/scala/chisel3/simulator/SimulatorAPI.scala b/src/main/scala/chisel3/simulator/SimulatorAPI.scala index 397f77d13b2..07fd78393e9 100644 --- a/src/main/scala/chisel3/simulator/SimulatorAPI.scala +++ b/src/main/scala/chisel3/simulator/SimulatorAPI.scala @@ -3,12 +3,91 @@ package chisel3.simulator import chisel3.{Module, RawModule} -import chisel3.simulator.stimulus.ResetProcedure +import chisel3.experimental.inlinetest.{HasTests, TestHarness} +import chisel3.simulator.stimulus.{InlineTestStimulus, ResetProcedure} import chisel3.testing.HasTestingDirectory import chisel3.util.simpleClassName import java.nio.file.Files trait SimulatorAPI { + object TestChoice { + + /** A choice of what test(s) to run. */ + sealed abstract class Type { + private[simulator] def globs: Seq[String] + require(globs.nonEmpty, "Must provide at least one test to run") + } + + /** Run tests matching any of these globs. */ + case class Globs(globs: Seq[String]) extends Type + + object Glob { + + /** Run tests matching a glob. */ + def apply(glob: String) = Names(Seq(glob)) + } + + /** Run tests matching any of these names. */ + case class Names(names: Seq[String]) extends Type { + override def globs = names + } + + object Name { + + /** Run tests matching this name. */ + def apply(name: String) = Names(Seq(name)) + } + + /** Run all tests. */ + case object All extends Type { + override def globs = Seq("*") + } + } + + object TestResult { + + /** A test result, either success or failure. */ + sealed trait Type + + /** Test passed. */ + case object Success extends Type + + /** Test failed with some exception. */ + case class Failure(error: Throwable) extends Type + } + + /** The results of a ChiselSim simulation of a module with tests. Contains results for one + * or more test simulations. */ + final class TestResults(digests: Seq[(String, Simulator.BackendInvocationDigest[_])]) { + private val results: Map[String, TestResult.Type] = digests.map { case (testName, digest) => + testName -> { + try { + digest.result + TestResult.Success + } catch { + case e: Throwable => TestResult.Failure(e) + } + } + }.toMap + + /** Get the result for the test with this name. */ + def apply(testName: String) = + results.lift(testName).getOrElse { + throw new NoSuchElementException(s"Cannot get result for ${testName} as it was not run") + } + + /** Return the names and results of tests that ran. */ + def all: Seq[(String, TestResult.Type)] = + results.toSeq + + /** Return the names and exceptions of tests that ran and failed. */ + def failed: Seq[(String, Throwable)] = + results.collect { case (name, TestResult.Failure(e)) => name -> e }.toSeq + + /** Return the names of tests that ran and passed. */ + def passed: Seq[String] = + results.collect { case r @ (name, TestResult.Success) => name }.toSeq + } /** Simulate a [[RawModule]] without any initialization procedure. * @@ -102,4 +181,55 @@ trait SimulatorAPI { stimulus(dut) } + /** Simulate the tests of a [[HasTests]] module. + * + * @param module the Chisel module to generate + * @param test the choice of which test(s) to run + * @param chiselOpts command line options to pass to Chisel + * @param firtoolOpts command line options to pass to firtool + * @param settings ChiselSim-related settings used for simulation + * @param additionalResetCycles a number of _additional_ cycles to assert + * reset for + * @param subdirectory an optional subdirectory for the test. This will be a + * subdirectory under what is provided by `testingDirectory`. + * @param stimulus directed stimulus to use + * @param testingDirectory a type class implementation that can be used to + * change the behavior of where files will be created + * + * @note Take care when passing `chiselOpts`. The following options are set + * by default and if you set incompatible options, the simulation will fail. + */ + def simulateTests[T <: RawModule with HasTests]( + module: => T, + tests: TestChoice.Type, + timeout: Int, + chiselOpts: Array[String] = Array.empty, + firtoolOpts: Array[String] = Array.empty, + settings: Settings[TestHarness[T, _]] = Settings.defaultRaw[TestHarness[T, _]], + subdirectory: Option[String] = None + )( + implicit hasSimulator: HasSimulator, + testingDirectory: HasTestingDirectory, + chiselOptsModifications: ChiselOptionsModifications, + firtoolOptsModifications: FirtoolOptionsModifications, + commonSettingsModifications: svsim.CommonSettingsModifications, + backendSettingsModifications: svsim.BackendSettingsModifications + ): TestResults = { + val modifiedTestingDirectory = subdirectory match { + case Some(subdir) => testingDirectory.withSubdirectory(subdir) + case None => testingDirectory + } + + new TestResults( + hasSimulator + .getSimulator(modifiedTestingDirectory) + .simulateTests( + module = module, + includeTestGlobs = tests.globs, + chiselOpts = chiselOpts, + firtoolOpts = firtoolOpts, + settings = settings + ) { dut => InlineTestStimulus(timeout)(dut.wrapped) } + ) + } } diff --git a/src/main/scala/chisel3/simulator/package.scala b/src/main/scala/chisel3/simulator/package.scala index 626a02f4144..c497ec37d9c 100644 --- a/src/main/scala/chisel3/simulator/package.scala +++ b/src/main/scala/chisel3/simulator/package.scala @@ -3,10 +3,12 @@ package chisel3 import svsim._ import chisel3.reflect.DataMirror import chisel3.experimental.dataview.reifyIdentityView +import chisel3.experimental.inlinetest.{HasTests, TestHarness} import chisel3.stage.DesignAnnotation import scala.collection.mutable import java.nio.file.{Files, Path, Paths} import firrtl.{annoSeqToSeq, seqToAnnoSeq} +import firrtl.AnnotationSeq package object simulator { @@ -131,14 +133,57 @@ package object simulator { args: Seq[String] = Seq.empty, firtoolArgs: Seq[String] = Seq.empty ): ElaboratedModule[T] = { - // Use CIRCT to generate SystemVerilog sources, and potentially additional artifacts - var someDut: Option[T] = None + val generated = generateWorkspaceSources(generateModule, args, firtoolArgs) + val ports = getModuleInfoPorts(generated.dut) + val moduleInfo = initializeModuleInfo(generated.dut, ports.map(_._2)) + val layers = generated.outputAnnotations.collectFirst { case DesignAnnotation(_, layers) => layers }.get + new ElaboratedModule(generated.dut, ports, layers) + } + + def elaborateAndMakeTestHarnessWorkspaces[T <: RawModule with HasTests]( + generateModule: () => T, + includeTestGlobs: Seq[String], + args: Seq[String] = Seq.empty, + firtoolArgs: Seq[String] = Seq.empty + ): Seq[(Workspace, String, ElaboratedModule[TestHarness[T, _]])] = { + val updatedArgs = args ++ includeTestGlobs.map("--include-tests-name=" + _) + val generated = generateWorkspaceSources(generateModule, updatedArgs, firtoolArgs) + generated.testHarnesses.map { case (testName, testHarness) => + val testWorkspace = workspace.shallowCopy(workspace.absolutePath + "/tests/" + testName) + val ports = getModuleInfoPorts(testHarness) + val moduleInfo = testWorkspace.initializeModuleInfo(testHarness, ports.map(_._2)) + val layers = generated.outputAnnotations.collectFirst { case DesignAnnotation(_, layers) => layers }.get + (testWorkspace, testName, new ElaboratedModule(testHarness.asInstanceOf[TestHarness[T, _]], ports, layers)) + } + } + + case class GeneratedWorkspaceInfo[T <: RawModule]( + dut: T, + testHarnesses: Seq[(String, TestHarness[_, _])], + outputAnnotations: AnnotationSeq + ) + + /** Use CIRCT to generate SystemVerilog sources, and potentially additional artifacts */ + def generateWorkspaceSources[T <: RawModule]( + generateModule: () => T, + args: Seq[String], + firtoolArgs: Seq[String] + ): GeneratedWorkspaceInfo[T] = { + var someDut: Option[() => T] = None + var someTestHarnesses: Option[() => Seq[(String, TestHarness[_, _])]] = None + val chiselArgs = Array("--target", "systemverilog", "--split-verilog") ++ args val outputAnnotations = (new circt.stage.ChiselStage).execute( - Array("--target", "systemverilog", "--split-verilog") ++ args, + chiselArgs, Seq( chisel3.stage.ChiselGeneratorAnnotation { () => val dut = generateModule() - someDut = Some(dut) + someDut = Some(() => dut) + someTestHarnesses = Some(() => + dut match { + case dut: HasTests => dut.getElaboratedTestModules + case _ => Nil + } + ) dut }, circt.stage.FirtoolOption("-disable-annotation-unknown"), @@ -197,53 +242,57 @@ package object simulator { .filter(_.getFileName.toString.startsWith("layers-")) .forEach(moveFile) - // Initialize Module Info - val dut = someDut.get - val ports = { + GeneratedWorkspaceInfo( + someDut.get(), + someTestHarnesses.get(), + outputAnnotations + ) + } + + def getModuleInfoPorts(dut: RawModule): Seq[(Data, ModuleInfo.Port)] = { - /** + /** * We infer the names of various ports since we don't currently have a good alternative when using MFC. We hope to replace this once we get better support from CIRCT. */ - def leafPorts(node: Data, name: String): Seq[(Data, ModuleInfo.Port)] = { - node match { - case record: Record => { - record.elements.toSeq.flatMap { case (fieldName, field) => - leafPorts(field, s"${name}_${fieldName}") - } + def leafPorts(node: Data, name: String): Seq[(Data, ModuleInfo.Port)] = { + node match { + case record: Record => { + record.elements.toSeq.flatMap { case (fieldName, field) => + leafPorts(field, s"${name}_${fieldName}") } - case vec: Vec[_] => { - vec.zipWithIndex.flatMap { case (element, index) => - leafPorts(element, s"${name}_${index}") - } + } + case vec: Vec[_] => { + vec.zipWithIndex.flatMap { case (element, index) => + leafPorts(element, s"${name}_${index}") } - case element: Element => - // Return the port only if the width is positive (firtool will optimized it out from the *.sv primary source) - if (element.widthKnown && element.getWidth > 0) { - DataMirror.directionOf(element) match { - case ActualDirection.Input => - Seq((element, ModuleInfo.Port(name, isGettable = true, isSettable = true))) - case ActualDirection.Output => Seq((element, ModuleInfo.Port(name, isGettable = true))) - case _ => Seq() - } - } else { - Seq() - } } - } - // Chisel ports can be Data or Property, but there is no ABI for Property ports, so we only return Data. - DataMirror.modulePorts(dut).flatMap { - case (name, data: Data) => leafPorts(data, name) - case _ => Nil + case element: Element => + // Return the port only if the width is positive (firtool will optimized it out from the *.sv primary source) + if (element.widthKnown && element.getWidth > 0) { + DataMirror.directionOf(element) match { + case ActualDirection.Input => + Seq((element, ModuleInfo.Port(name, isGettable = true, isSettable = true))) + case ActualDirection.Output => Seq((element, ModuleInfo.Port(name, isGettable = true))) + case _ => Seq() + } + } else { + Seq() + } } } - workspace.elaborate( - ModuleInfo( - name = dut.name, - ports = ports.map(_._2) - ) + // Chisel ports can be Data or Property, but there is no ABI for Property ports, so we only return Data. + DataMirror.modulePorts(dut).flatMap { + case (name, data: Data) => leafPorts(data, name) + case _ => Nil + } + } + + def initializeModuleInfo(dut: RawModule, ports: Seq[ModuleInfo.Port]): Unit = { + val info = ModuleInfo( + name = dut.name, + ports = ports ) - val layers = outputAnnotations.collectFirst { case DesignAnnotation(_, layers) => layers }.get - new ElaboratedModule(dut, ports, layers) + workspace.elaborate(info) } } } diff --git a/src/main/scala/chisel3/simulator/stimulus/InlineTestStimulus.scala b/src/main/scala/chisel3/simulator/stimulus/InlineTestStimulus.scala new file mode 100644 index 00000000000..4bfa62baee6 --- /dev/null +++ b/src/main/scala/chisel3/simulator/stimulus/InlineTestStimulus.scala @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 + +package chisel3.simulator.stimulus + +import chisel3.{Clock, Module, RawModule, Reset} +import chisel3.simulator.{AnySimulatedModule, Exceptions} +import chisel3.simulator.stimulus.Stimulus +import chisel3.experimental.inlinetest.TestHarness + +trait InlineTestStimulus extends Stimulus.Type[TestHarness[_, _]] { + protected def _timeout: Int + + override final def apply(dut: TestHarness[_, _]): Unit = { + val module = AnySimulatedModule.current + val controller = module.controller + + val clock = module.port(dut.clock) + val reset = module.port(dut.reset) + val finish = module.port(dut.io.finish) + val success = module.port(dut.io.success) + + controller.run(1) + reset.set(0) + controller.run(1) + reset.set(1) + + clock.tick( + timestepsPerPhase = 1, + maxCycles = 1, + inPhaseValue = 1, + outOfPhaseValue = 0, + sentinel = None + ) + + reset.set(0) + + var cycleCount = 0 + + while (finish.get().asBigInt == 0) { + clock.tick( + timestepsPerPhase = 1, + maxCycles = 1, + inPhaseValue = 1, + outOfPhaseValue = 0, + sentinel = None + ) + cycleCount += 1 + + if (cycleCount > _timeout) { + throw new Exceptions.Timeout(_timeout, s"Test did not assert finish before ${_timeout} timesteps") + } + } + + if (success.get().asBigInt == 0) { + throw new Exceptions.TestFailed + } + } +} + +object InlineTestStimulus { + def apply(timeout: Int) = new InlineTestStimulus { + override val _timeout = timeout + } +} diff --git a/src/test/scala/chiselTests/experimental/InlineTestSpec.scala b/src/test/scala/chiselTests/experimental/InlineTestSpec.scala index 6e61031f227..09b1fd68ade 100644 --- a/src/test/scala/chiselTests/experimental/InlineTestSpec.scala +++ b/src/test/scala/chiselTests/experimental/InlineTestSpec.scala @@ -1,16 +1,18 @@ package chiselTests -import chisel3._ +import chisel3.{assert => _, _} import chisel3.experimental.hierarchy._ import chisel3.experimental.inlinetest._ +import chisel3.simulator.scalatest.ChiselSim import chisel3.testers._ +import chisel3.properties.Property import chisel3.testing.scalatest.FileCheck -import chisel3.util.Enum +import chisel3.util.{Counter, Enum} import circt.stage.ChiselStage import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers -import circt.stage.ChiselStage.emitCHIRRTL +import circt.stage.ChiselStage.{emitCHIRRTL, emitSystemVerilog} // Here is a testharness that expects some sort of interface on its DUT, e.g. a probe // socket to which to attach a monitor. @@ -21,7 +23,7 @@ class TestHarnessWithMonitorSocket[M <: RawModule with HasMonitorSocket](test: T } object TestHarnessWithMonitorSocket { - implicit def testharnessGenerator[M <: RawModule with HasMonitorSocket] = + implicit def generator[M <: RawModule with HasMonitorSocket] = TestHarnessGenerator[M, Unit](new TestHarnessWithMonitorSocket(_)) } @@ -42,7 +44,7 @@ class ProtocolBundle(width: Int) extends Bundle { class ProtocolMonitor(bundleType: ProtocolBundle) extends Module { val io = IO(Input(bundleType)) - assert(io.in === io.out, "in === out") + chisel3.assert(io.in === io.out, "in === out") } @instantiable @@ -55,19 +57,39 @@ trait HasProtocolInterface extends HasTests { this: RawModule => object ProtocolChecks { def check(v: Int)(instance: Instance[RawModule with HasProtocolInterface]) = { instance.io.in := v.U - assert(instance.io.out === v.U): Unit + chisel3.assert(instance.io.out === v.U): Unit + } +} + +trait HasTestsProperty { this: RawModule with HasTests => + def enableTestsProperty: Boolean + + val testNames = Option.when(enableTestsProperty) { + IO(Output(Property[Seq[String]]())) + } + + atModuleBodyEnd { + testNames.foreach { testNames => + testNames := Property { + getRegisteredTests.flatMap { case (test, willElaborate) => + Option.when(willElaborate)(test.testName) + } + } + } } } @instantiable class ModuleWithTests( - ioWidth: Int = 32, - override val resetType: Module.ResetType.Type = Module.ResetType.Synchronous, - override val elaborateTests: Boolean = true + ioWidth: Int = 32, + override val resetType: Module.ResetType.Type = Module.ResetType.Synchronous, + override val elaborateTests: Boolean = true, + override val enableTestsProperty: Boolean = false ) extends Module with HasMonitorSocket with HasTests - with HasProtocolInterface { + with HasProtocolInterface + with HasTestsProperty { @public val io = IO(new ProtocolBundle(ioWidth)) override val monProbe = makeProbe(io) @@ -76,12 +98,12 @@ class ModuleWithTests( test("foo") { instance => instance.io.in := 3.U(ioWidth.W) - assert(instance.io.out === 3.U): Unit + chisel3.assert(instance.io.out === 3.U): Unit } test("bar") { instance => instance.io.in := 5.U(ioWidth.W) - assert(instance.io.out =/= 0.U): Unit + chisel3.assert(instance.io.out =/= 0.U): Unit } test("with_result") { instance => @@ -104,11 +126,54 @@ class ModuleWithTests( import TestHarnessWithMonitorSocket._ test("with_monitor") { instance => instance.io.in := 5.U(ioWidth.W) - assert(instance.io.out =/= 0.U): Unit + chisel3.assert(instance.io.out =/= 0.U): Unit } } test("check2")(ProtocolChecks.check(2)) + + test("signal_pass") { instance => + val counter = Counter(16) + counter.inc() + instance.io.in := counter.value + val result = Wire(new TestResultBundle()) + result.success := true.B + result.finish := counter.value === 15.U + result + } + + test("signal_pass_2") { instance => + val counter = Counter(16) + counter.inc() + instance.io.in := counter.value + val result = Wire(new TestResultBundle()) + result.success := true.B + result.finish := counter.value === 15.U + result + } + + test("signal_fail") { instance => + val counter = Counter(16) + counter.inc() + instance.io.in := counter.value + val result = Wire(new TestResultBundle()) + result.success := false.B + result.finish := counter.value === 15.U + result + } + + test("timeout") { instance => + val counter = Counter(16) + counter.inc() + instance.io.in := counter.value + } + + test("assertion") { instance => + val counter = Counter(16) + counter.inc() + instance.io.in := counter.value + chisel3.assert(instance.io.out < 15.U, "counter hit max"): Unit + } } @instantiable @@ -117,11 +182,11 @@ class RawModuleWithTests(ioWidth: Int = 32) extends RawModule with HasTests { io.out := io.in test("foo") { instance => instance.io.in := 3.U(ioWidth.W) - assert(instance.io.out === 3.U): Unit + chisel3.assert(instance.io.out === 3.U): Unit } } -class InlineTestSpec extends AnyFlatSpec with FileCheck { +class InlineTestSpec extends AnyFlatSpec with FileCheck with ChiselSim { private def makeArgs(moduleGlobs: Seq[String], testGlobs: Seq[String]): Array[String] = ( moduleGlobs.map { glob => s"--include-tests-module=$glob" } ++ @@ -299,8 +364,7 @@ class InlineTestSpec extends AnyFlatSpec with FileCheck { } it should "compile to verilog" in { - ChiselStage - .emitSystemVerilog(new ModuleWithTests, args = argsElaborateAllTests) + emitSystemVerilog(new ModuleWithTests, args = argsElaborateAllTests) .fileCheck()( """ | CHECK: module ModuleWithTests @@ -314,6 +378,20 @@ class InlineTestSpec extends AnyFlatSpec with FileCheck { ) } + it should "support iterating over registered tests to capture metadata" in { + ChiselStage + .emitCHIRRTL(new ModuleWithTests(enableTestsProperty = true), args = makeArgs(Seq("*"), Seq("foo", "bar"))) + .fileCheck()( + """ + | CHECK: module ModuleWithTests + | CHECK: output testNames : List + | CHECK: propassign testNames, List(String("foo"), String("bar")) + | CHECK: module test_ModuleWithTests_foo + | CHECK: module test_ModuleWithTests_bar + """ + ) + } + it should "emit the correct reset types" in { def fileCheckString(resetType: String) = s""" @@ -379,4 +457,121 @@ class InlineTestSpec extends AnyFlatSpec with FileCheck { """ ) } + + def assertPass(result: TestResult.Type): Unit = result match { + case TestResult.Success => () + case TestResult.Failure(e) => throw e + } + + def assertFail(result: TestResult.Type): Unit = result match { + case TestResult.Success => fail("Test unexpectedly passed") + case TestResult.Failure(e) => + e.getMessage() + .fileCheck() { + """ + | CHECK: The test finished and signaled failure + """ + } + } + + def assertTimeout(timeout: Int)(result: TestResult.Type): Unit = result match { + case TestResult.Success => fail("Test unexpectedly passed") + case TestResult.Failure(e) => + e.getMessage() + .fileCheck() { + s""" + | CHECK: A timeout occurred after ${timeout} timesteps + """ + } + } + + def assertAssertion(message: String)(result: TestResult.Type): Unit = result match { + case TestResult.Success => fail("Test unexpectedly passed") + case TestResult.Failure(e) => + e.getMessage() + .fileCheck() { + """ + | CHECK: One or more assertions failed during Chiselsim simulation + | CHECK: counter hit max + """ + } + } + + it should "simulate and pass if finish asserted with success=1" in { + val results = simulateTests( + new ModuleWithTests, + tests = TestChoice.Name("signal_pass"), + timeout = 100 + ) + assertPass(results("signal_pass")) + } + + it should "simulate and fail if finish asserted with success=0" in { + val results = simulateTests( + new ModuleWithTests, + tests = TestChoice.Name("signal_fail"), + timeout = 100 + ) + assertFail(results("signal_fail")) + } + + it should "simulate and timeout if finish not asserted" in { + val results = simulateTests( + new ModuleWithTests, + tests = TestChoice.Name("timeout"), + timeout = 100 + ) + assertTimeout(100)(results("timeout")) + } + + it should "simulate and fail early if assertion raised" in { + val results = simulateTests( + new ModuleWithTests, + tests = TestChoice.Name("assertion"), + timeout = 100 + ) + assertAssertion("counter hit max")(results("assertion")) + } + + it should "run multiple passing simulations" in { + val results = simulateTests( + new ModuleWithTests, + tests = TestChoice.Names(Seq("signal_pass", "signal_pass_2")), + timeout = 100 + ) + results.all.foreach { case (name, result) => + assertPass(result) + } + } + + it should "run one passing and one failing simulation" in { + val results = simulateTests( + new ModuleWithTests, + tests = TestChoice.Names(Seq("signal_pass", "signal_fail")), + timeout = 100 + ) + assertPass(results("signal_pass")) + assertFail(results("signal_fail")) + } + + it should "simulate all tests" in { + val results = simulateTests( + new ModuleWithTests, + tests = TestChoice.All, + timeout = 100 + ) + assert(results.all.size == 11) + + assertFail(results("signal_fail")) + assertTimeout(100)(results("timeout")) + assertAssertion("counter hit max")(results("assertion")) + assertTimeout(100)(results("check1")) + assertTimeout(100)(results("check2")) + assertTimeout(100)(results("bar")) + assertPass(results("signal_pass")) + assertPass(results("signal_pass_2")) + assertTimeout(100)(results("with_monitor")) + assertTimeout(100)(results("with_result")) + assertTimeout(100)(results("foo")) + } } diff --git a/svsim/src/main/scala/Workspace.scala b/svsim/src/main/scala/Workspace.scala index cb4a9ff36d9..4e9504ccd2d 100644 --- a/svsim/src/main/scala/Workspace.scala +++ b/svsim/src/main/scala/Workspace.scala @@ -50,7 +50,7 @@ final class Workspace( else s"${System.getProperty("user.dir")}/$path" - /** A directory where the user can store additional artifacts which are relevant to the primary sources (for instance, artifacts related to the generation of primary sources). These artifacts have no impact on the simulation, but it may be useful to group them with the other files generated by svsim for debugging purposes. + /** A directory where the user can store additional artifacts which are relevant to the primary sources (for instance, artifacts related to the generation of primary sources). These artifacts have no impact on the simulation, but it may be useful to group them with the other files generated by svsim for debugging purposes. artifacts related to the generation of primary sources). These artifacts have no impact on the simulation, but it may be useful to group them with the other files generated by svsim for debugging purposes. */ val supportArtifactsPath = s"$absolutePath/support-artifacts" @@ -329,6 +329,31 @@ final class Workspace( } //format: on + /** Shallow copy the sources from this workspace to a new one. Primary sources are symlinked to the + * new directory; nothing else is copied. + */ + def shallowCopy(newPath: String, workingDirectoryPrefix: String = this.workingDirectoryPrefix): Workspace = { + val newWorkspace = new Workspace(newPath, workingDirectoryPrefix) + newWorkspace.reset() + + val newPrimarySources = new File(newWorkspace.primarySourcesPath) + + val sourcePrimarySources = new File(this.primarySourcesPath) + if (sourcePrimarySources.exists()) { + Files + .walk(sourcePrimarySources.toPath) + .filter(Files.isRegularFile(_)) + .forEach { source => + val relativePath = sourcePrimarySources.toPath.relativize(source) + val targetPath = Paths.get(newPrimarySources.getPath, relativePath.toString) + targetPath.getParent.toFile.mkdirs() + Files.createSymbolicLink(targetPath, source) + } + } + + newWorkspace + } + /** Compiles the simulation using the specified backend. * * @param outputTag A string which will be used to tag the output directory. This enables compiling and simulating the same workspace with multiple backends.