diff options
| author | Albert Magyar | 2019-10-16 01:40:34 -0700 |
|---|---|---|
| committer | Albert Magyar | 2019-10-21 10:02:46 -0700 |
| commit | 24df1cdcc0981f6e32662a42fe01135681db18c4 (patch) | |
| tree | c1836e689123737813622ac24139f8370d0cd719 /src/test | |
| parent | 01b10725163c5bbe239c11f14b6136c737160d34 (diff) | |
Add library for streamlined Verilog execution tests
Diffstat (limited to 'src/test')
4 files changed, 280 insertions, 0 deletions
diff --git a/src/test/scala/firrtlTests/execution/ExecutionTestHelper.scala b/src/test/scala/firrtlTests/execution/ExecutionTestHelper.scala new file mode 100644 index 00000000..7d250664 --- /dev/null +++ b/src/test/scala/firrtlTests/execution/ExecutionTestHelper.scala @@ -0,0 +1,112 @@ +package firrtlTests.execution + +import firrtl._ +import firrtl.ir._ + +object DUTRules { + val dutName = "dut" + val clock = Reference("clock", ClockType) + val reset = Reference("reset", Utils.BoolType) + val counter = Reference("step", UnknownType) + + // Need a flat name for the register that latches poke values + val illegal = raw"[\[\]\.]".r + val pokeRegSuffix = "_poke" + def pokeRegName(e: Expression) = illegal.replaceAllIn(e.serialize, "_") + pokeRegSuffix + + // Naming patterns are static, so DUT has to be checked for proper form + collisions + def hasNameConflicts(c: Circuit): Boolean = { + val top = c.modules.find(_.name == c.main).get + val names = Namespace(top).cloneUnderlying + names.contains(counter.name) || names.exists(_.contains(pokeRegSuffix)) + } +} + +object ExecutionTestHelper { + val counterType = UIntType(IntWidth(32)) + def apply(body: String): ExecutionTestHelper = { + // Parse input and check that it complies with test syntax rules + val c = ParseStatement.makeDUT(body) + require(!DUTRules.hasNameConflicts(c), "Avoid using 'step' or 'poke' in DUT component names") + + // Generate test step counter, create ExecutionTestHelper that represents initial test state + val cnt = DefRegister(NoInfo, DUTRules.counter.name, counterType, DUTRules.clock, DUTRules.reset, Utils.zero) + val inc = Connect(NoInfo, DUTRules.counter, DoPrim(PrimOps.Add, Seq(DUTRules.counter, UIntLiteral(1)), Nil, UnknownType)) + ExecutionTestHelper(c, Seq(cnt, inc), Map.empty[Expression, Expression], Nil, Nil) + } +} + +case class ExecutionTestHelper( + dut: Circuit, + setup: Seq[Statement], + pokeRegs: Map[Expression, Expression], + completedSteps: Seq[Conditionally], + activeStep: Seq[Statement] +) { + + def step(n: Int): ExecutionTestHelper = { + require(n > 0, "Step length must be positive") + (0 until n).foldLeft(this) { case (eth, int) => eth.next } + } + + def poke(expString: String, value: Literal): ExecutionTestHelper = { + val pokeExp = ParseExpression(expString) + val pokeable = ensurePokeable(pokeExp) + pokeable.addStatements( + Connect(NoInfo, pokeExp, value), + Connect(NoInfo, pokeable.pokeRegs(pokeExp), value)) + } + + def invalidate(expString: String): ExecutionTestHelper = { + addStatements(IsInvalid(NoInfo, ParseExpression(expString))) + } + + def expect(expString: String, value: Literal): ExecutionTestHelper = { + val peekExp = ParseExpression(expString) + val neq = DoPrim(PrimOps.Neq, Seq(peekExp, value), Nil, Utils.BoolType) + addStatements(Stop(NoInfo, 1, DUTRules.clock, neq)) + } + + def finish(): ExecutionTestHelper = { + addStatements(Stop(NoInfo, 0, DUTRules.clock, Utils.one)).next + } + + // Private helper methods + + private def t = completedSteps.length + + private def addStatements(stmts: Statement*) = copy(activeStep = activeStep ++ stmts) + + private def next: ExecutionTestHelper = { + val count = Reference(DUTRules.counter.name, DUTRules.counter.tpe) + val ifStep = DoPrim(PrimOps.Eq, Seq(count, UIntLiteral(t)), Nil, Utils.BoolType) + val onThisStep = Conditionally(NoInfo, ifStep, Block(activeStep), EmptyStmt) + copy(completedSteps = completedSteps :+ onThisStep, activeStep = Nil) + } + + private def top: Module = { + dut.modules.collectFirst({ case m: Module if m.name == dut.main => m }).get + } + + private[execution] def emit: Circuit = { + val finished = finish() + val modulesX = dut.modules.collect { + case m: Module if m.name == dut.main => + m.copy(body = Block(m.body +: (setup ++ finished.completedSteps))) + case m => m + } + dut.copy(modules = modulesX) + } + + private def ensurePokeable(pokeExp: Expression): ExecutionTestHelper = { + if (pokeRegs.contains(pokeExp)) { + this + } else { + val pName = DUTRules.pokeRegName(pokeExp) + val pRef = Reference(pName, UnknownType) + val pReg = DefRegister(NoInfo, pName, UIntType(UnknownWidth), DUTRules.clock, Utils.zero, pRef) + val defaultConn = Connect(NoInfo, pokeExp, pRef) + copy(setup = setup ++ Seq(pReg, defaultConn), pokeRegs = pokeRegs + (pokeExp -> pRef)) + } + } +} diff --git a/src/test/scala/firrtlTests/execution/ParserHelpers.scala b/src/test/scala/firrtlTests/execution/ParserHelpers.scala new file mode 100644 index 00000000..3472c19c --- /dev/null +++ b/src/test/scala/firrtlTests/execution/ParserHelpers.scala @@ -0,0 +1,52 @@ +package firrtlTests.execution + +import firrtl._ +import firrtl.ir._ + +class ParserHelperException(val pe: ParserException, input: String) + extends FirrtlUserException(s"Got error ${pe.toString} while parsing input:\n${input}") + +/** + * A utility class that parses a FIRRTL string representing a statement to a sub-AST + */ +object ParseStatement { + private def wrapStmtStr(stmtStr: String): String = { + val indent = " " + val indented = stmtStr.split("\n").mkString(indent, s"\n${indent}", "") + s"""circuit ${DUTRules.dutName} : + | module ${DUTRules.dutName} : + | input clock : Clock + | input reset : UInt<1> + |${indented}""".stripMargin + } + + private def parse(stmtStr: String): Circuit = { + try { + Parser.parseString(wrapStmtStr(stmtStr), Parser.IgnoreInfo) + } catch { + case e: ParserException => throw new ParserHelperException(e, stmtStr) + } + } + + def apply(stmtStr: String): Statement = { + val c = parse(stmtStr) + val stmt = c.modules.collectFirst { case Module(_, _, _, b: Block) => b.stmts.head } + stmt.get + } + + private[execution] def makeDUT(body: String): Circuit = parse(body) +} + +/** + * A utility class that parses a FIRRTL string representing an expression to a sub-AST + */ +object ParseExpression { + def apply(expStr: String): Expression = { + try { + val s = ParseStatement(s"${expStr} is invalid") + s.asInstanceOf[IsInvalid].expr + } catch { + case e: ParserHelperException => throw new ParserHelperException(e.pe, expStr) + } + } +} diff --git a/src/test/scala/firrtlTests/execution/SimpleExecutionTest.scala b/src/test/scala/firrtlTests/execution/SimpleExecutionTest.scala new file mode 100644 index 00000000..5abeb819 --- /dev/null +++ b/src/test/scala/firrtlTests/execution/SimpleExecutionTest.scala @@ -0,0 +1,84 @@ +package firrtlTests.execution + +import java.io.File + +import firrtl.ir._ +import firrtlTests._ + +sealed trait SimpleTestCommand +case class Step(n: Int) extends SimpleTestCommand +case class Invalidate(expStr: String) extends SimpleTestCommand +case class Poke(expStr: String, value: Int) extends SimpleTestCommand +case class Expect(expStr: String, value: Int) extends SimpleTestCommand + +/** + * This trait defines an interface to run a self-contained test circuit. + */ +trait TestExecution { + def runEmittedDUT(c: Circuit, testDir: File): Unit +} + +/** + * A class that makes it easier to write execution-driven tests. + * + * By combining a DUT body (supplied as a string without an enclosing + * module or circuit) with a sequence of test operations, an + * executable, self-contained Verilog testbench may be automatically + * created and checked. + * + * @note It is necessary to mix in a trait extending TestExecution + * @note The DUT has two implicit ports, "clock" and "reset" + * @note Execution of the command sequences begins after reset is deasserted + * + * @see [[firrtlTests.execution.TestExecution]] + * @see [[firrtlTests.execution.VerilogExecution]] + * + * @example {{{ + * class AndTester extends SimpleExecutionTest with VerilogExecution { + * val body = "reg r : UInt<32>, clock with: (reset => (reset, UInt<32>(0)))" + * val commands = Seq( + * Expect("r", 0), + * Poke("r", 3), + * Step(1), + * Expect("r", 3) + * ) + * } + * }}} + */ +abstract class SimpleExecutionTest extends FirrtlPropSpec { + this: TestExecution => + + /** + * Text representing the body of the DUT. This is useful for testing + * statement-level language features, and cuts out the overhead of + * writing a top-level DUT module and having peeks/pokes point at + * IOs. + */ + val body: String + + /** + * A sequence of commands (peeks, pokes, invalidates, steps) that + * represents how the testbench will progress. The semantics are + * inspired by chisel-testers. + */ + def commands: Seq[SimpleTestCommand] + + private def interpretCommand(eth: ExecutionTestHelper, cmd: SimpleTestCommand) = cmd match { + case Step(n) => eth.step(n) + case Invalidate(expStr) => eth.invalidate(expStr) + case Poke(expStr, value) => eth.poke(expStr, UIntLiteral(value)) + case Expect(expStr, value) => eth.expect(expStr, UIntLiteral(value)) + } + + private def runTest(): Unit = { + val initial = ExecutionTestHelper(body) + val test = commands.foldLeft(initial)(interpretCommand(_, _)) + val testName = this.getClass.getSimpleName + val testDir = createTestDirectory(s"${testName}-generated-src") + runEmittedDUT(test.emit, testDir) + } + + property("Execution of the compiled Verilog for ExecutionTestHelper should succeed") { + runTest() + } +} diff --git a/src/test/scala/firrtlTests/execution/VerilogExecution.scala b/src/test/scala/firrtlTests/execution/VerilogExecution.scala new file mode 100644 index 00000000..17eecc65 --- /dev/null +++ b/src/test/scala/firrtlTests/execution/VerilogExecution.scala @@ -0,0 +1,32 @@ +package firrtlTests.execution + +import java.io.File + +import firrtl._ +import firrtl.ir._ +import firrtlTests._ + +import firrtl.stage.{FirrtlCircuitAnnotation, FirrtlStage} +import firrtl.options.TargetDirAnnotation + +/** + * Mixing in this trait causes a SimpleExecutionTest to be run in Verilog simulation. + */ +trait VerilogExecution extends TestExecution { + this: SimpleExecutionTest => + def runEmittedDUT(c: Circuit, testDir: File): Unit = { + // Run FIRRTL, emit Verilog file + val cAnno = FirrtlCircuitAnnotation(c) + val tdAnno = TargetDirAnnotation(testDir.getAbsolutePath) + (new FirrtlStage).run(AnnotationSeq(Seq(cAnno, tdAnno))) + + // Copy harness resource to test directory + val harness = new File(testDir, s"top.cpp") + copyResourceToFile(cppHarnessResourceName, harness) + + // Make and run Verilog simulation + verilogToCpp(c.main, testDir, Nil, harness).! + cppToExe(c.main, testDir).! + assert(executeExpectingSuccess(c.main, testDir)) + } +} |
