Search code examples
scalachiselchiseltest

Where does dut in Chisel Test get defined? (About Scala syntax)


I am trying to come up with a better title.
I am new in Chisel and Scala. Below there is a Chisel code defining and testing an module.

import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec

class DeviceUnderTest extends Module {
  val io = IO(new Bundle {
    val a = Input(UInt(2.W))
    val b = Input(UInt(2.W))
    val out = Output(UInt(2.W))
  })
  io.out := io.a & io.b
}

class WaveformTestWithIteration extends AnyFlatSpec with ChiselScalatestTester {
  "WaveformIteration" should "pass" in {
    test(new DeviceUnderTest)
      .withAnnotations(Seq(WriteVcdAnnotation)) ( dut => // ???
        {
          for (a <- 0 until 4; b <- 0 until 4) {
            dut.io.a.poke(a.U)
            dut.io.b.poke(b.U)
            dut.clock.step()
          }
        }
      )
  }
}

The code line with comments ??? is where I am quite puzzled. Where does the variable dut is defined? It seems an reference to instance gained by new DeviceUnderTest.

test(new DeviceUnderTest).withAnnotations(Seq(WriteVcdAnnotation)) return an TestBuilder[T] with apply method:

  class TestBuilder[T <: Module](...) {
    ...
    def apply(testFn: T => Unit): TestResult = {
      runTest(defaults.createDefaultTester(dutGen, finalAnnos))(testFn)
    }
    ...
  }

So, dut => {...} is a function (T) => Unit? But it does not look like a standard lambda ((x:T) => {...})? Or it is something else?
What is this syntax in scala exactly?


Solution

  • Consider the following boiled-down version with a similar structure:

    def test[A](testedThing: A)(testBody: A => Unit): Unit = testBody(testedThing)
    

    What it's essentially doing is taking a value x: A and a function f: A => Unit, and applying f to x to obtain f(x).

    Here is how you could use it:

    test("foo"){ x => 
      println(if x == "foo" then "Success" else "Failure")
    } // Success
    
    test("bar"){ x => 
      println(if x == "baz" then "Success" else "Failure")
    } // Failure
    

    In both cases, the "string under test" is simply passed to the body of the "test".

    Now, you could introduce a few more steps between the creation of the value under test and the specification of the body. For example, you could create a TestBuilder[A], which is essentially just a value a with some bells and whistles (in this case, list of "annotations" - plain strings in the following example):

    type Annotation = String
    
    case class TestOutcome(annotations: List[Annotation], successful: Boolean)
    
    trait Test:
      def run: TestOutcome
    
    // This simply captures the value under test of type `A`
    case class TestBuilder[A](
      theThingUnderTest: A,
      annotations: List[Annotation]
    ):
      // Bells and whistles: adding some metadata
      def withAnnotations(moreAnnotations: List[Annotation]): TestBuilder[A] =
        TestBuilder(theThingUnderTest, annotations ++ moreAnnotations)
    
      // Combining the value under test with the body of the test produces the
      // actual test
      def apply(testBody: A => Unit): Test = new Test:
        def run =
          try {
            testBody(theThingUnderTest)
            TestOutcome(annotations, true)
          } catch {
            case t: Throwable => TestOutcome(annotations, false)
          }
    
    // This constructs the thing that's being tested, and creates a TestBuilder around it
    def test[A](thingUnderTest: A) = TestBuilder(thingUnderTest, Nil)
    
    println(
      test("hello")
        .withAnnotations(List("size of hello should be 5")){ h =>
          assert(h.size == 5)
        }
        .run
    )
    
    println(
      test("hello")
        .withAnnotations(List("size of hello should be 42")){ h =>
          assert(h.size == 42)
        }
        .run
    )
    

    The principle remains the same: test(a) saves the tested value a, then TestBuilder adds some configuration, and once you add a body { thingUnderTest => /* assertStuff */ } to it, you get a full Test, which you can then run to obtain some results (TestOutcomes, in this case). Thus, the above snippet produces

    TestOutcome(List(size of hello should be 5),true)
    TestOutcome(List(size of hello should be 42),false)