summaryrefslogtreecommitdiff
path: root/src/test/scala/chiselTests/stage/ChiselMainSpec.scala
blob: e877311144af21f6fe4e0a137666c3540aa5c9c9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
// SPDX-License-Identifier: Apache-2.0

package chiselTests.stage

import chisel3._
import chisel3.stage.ChiselMain
import java.io.File

import chisel3.aop.inspecting.{InspectingAspect, InspectorAspect}
import org.scalatest.GivenWhenThen
import org.scalatest.featurespec.AnyFeatureSpec
import org.scalatest.matchers.should.Matchers
import org.scalatest.Inside._
import org.scalatest.EitherValues._

import scala.io.Source
import firrtl.Parser

object ChiselMainSpec {

  /** A module that connects two different types together resulting in an elaboration error */
  class DifferentTypesModule extends RawModule {
    val in = IO(Input(UInt(1.W)))
    val out = IO(Output(SInt(1.W)))
    out := in
  }

  /** A module that connects two of the same types together */
  class SameTypesModule extends Module {
    val in = IO(Input(UInt(1.W)))
    val out = IO(Output(UInt(1.W)))
    out := in
  }

  /** A module that fails a requirement */
  class FailingRequirementModule extends RawModule {
    require(false, "the user wrote a failing requirement")
  }

  /** A module that triggers a Builder.error (as opposed to exception) */
  class BuilderErrorModule extends RawModule {
    val w = Wire(UInt(8.W))
    w(3, -1)
  }
}

case class TestClassAspect()
    extends InspectorAspect[RawModule]({ _: RawModule =>
      println("Ran inspectingAspect")
    })

case object TestObjectAspect
    extends InspectorAspect[RawModule]({ _: RawModule =>
      println("Ran inspectingAspect")
    })

class ChiselMainSpec extends AnyFeatureSpec with GivenWhenThen with Matchers with chiselTests.Utils {

  import ChiselMainSpec._

  class ChiselMainFixture {
    Given("a Chisel stage")
    val stage = ChiselMain
  }

  class TargetDirectoryFixture(dirName: String) {
    val dir = new File(s"test_run_dir/ChiselStageSpec/$dirName")
    val buildDir = new File(dir + "/build")
    dir.mkdirs()
  }

  case class ChiselMainTest(
    args:       Array[String],
    generator:  Option[Class[_ <: RawModule]] = None,
    files:      Seq[String] = Seq.empty,
    stdout:     Seq[Either[String, String]] = Seq.empty,
    stderr:     Seq[Either[String, String]] = Seq.empty,
    result:     Int = 0,
    fileChecks: Map[String, File => Unit] = Map.empty) {
    def testName:   String = "args" + args.mkString("_")
    def argsString: String = args.mkString(" ")
  }

  /** A test of ChiselMain that is going to involve catching an exception.
    * @param args command line arguments (excluding --module) to pass in
    * @param generator the module to build (used to generate --module)
    * @param message snippets of text that should appear (Right) or not appear (Left) in the exception message
    * @param stdout snippets of text that should appear (Right) or not appear (Left) in STDOUT
    * @param stderr snippets of text that should appear (Right) or not appear (Left) in STDERR
    * @param stackTrace snippets of text that should appear (Right) or not appear (Left) in the stack trace
    * @tparam the type of exception that should occur
    */
  case class ChiselMainExceptionTest[A <: Throwable](
    args:       Array[String],
    generator:  Option[Class[_ <: RawModule]] = None,
    message:    Seq[Either[String, String]] = Seq.empty,
    stdout:     Seq[Either[String, String]] = Seq.empty,
    stderr:     Seq[Either[String, String]] = Seq.empty,
    stackTrace: Seq[Either[String, String]] = Seq.empty) {
    def testName:   String = "args" + args.mkString("_")
    def argsString: String = args.mkString(" ")
  }

  def runStageExpectFiles(p: ChiselMainTest): Unit = {
    Scenario(s"""User runs Chisel Stage with '${p.argsString}'""") {
      val f = new ChiselMainFixture
      val td = new TargetDirectoryFixture(p.testName)

      p.files.foreach(f => new File(td.buildDir + s"/$f").delete())

      When(s"""the user tries to compile with '${p.argsString}'""")
      val module: Array[String] =
        (if (p.generator.nonEmpty) { Array("--module", p.generator.get.getName) }
         else { Array.empty[String] })
      f.stage.main(Array("-td", td.buildDir.toString) ++ module ++ p.args)
      val (stdout, stderr, result) =
        grabStdOutErr {
          catchStatus {
            f.stage.main(Array("-td", td.buildDir.toString) ++ module ++ p.args)
          }
        }

      p.stdout.foreach {
        case Right(a) =>
          Then(s"""STDOUT should include "$a"""")
          stdout should include(a)
        case Left(a) =>
          Then(s"""STDOUT should not include "$a"""")
          (stdout should not).include(a)
      }

      p.stderr.foreach {
        case Right(a) =>
          Then(s"""STDERR should include "$a"""")
          stderr should include(a)
        case Left(a) =>
          Then(s"""STDERR should not include "$a"""")
          (stderr should not).include(a)
      }

      p.result match {
        case 0 =>
          And(s"the exit code should be 0")
          result shouldBe a[Right[_, _]]
        case a =>
          And(s"the exit code should be $a")
          result shouldBe (Left(a))
      }

      p.files.foreach { f =>
        And(s"file '$f' should be emitted in the target directory")
        val out = new File(td.buildDir + s"/$f")
        out should (exist)
        p.fileChecks.get(f).map(_(out))
      }
    }
  }

  /** Run a ChiselMainExceptionTest and verify that all the properties it spells out hold.
    * @param p the test to run
    * @tparam the type of the exception to catch (you shouldn't have to explicitly provide this)
    */
  def runStageExpectException[A <: Throwable: scala.reflect.ClassTag](p: ChiselMainExceptionTest[A]): Unit = {
    Scenario(s"""User runs Chisel Stage with '${p.argsString}'""") {
      val f = new ChiselMainFixture
      val td = new TargetDirectoryFixture(p.testName)

      When(s"""the user tries to compile with '${p.argsString}'""")
      val module: Array[String] =
        (if (p.generator.nonEmpty) { Array("--module", p.generator.get.getName) }
         else { Array.empty[String] })
      val (stdout, stderr, result) =
        grabStdOutErr {
          catchStatus {
            intercept[A] {
              f.stage.main(Array("-td", td.buildDir.toString) ++ module ++ p.args)
            }
          }
        }

      Then("the expected exception was thrown")
      (result should be).a('right)
      val exception = result.right.get
      info(s"""  - Exception was a "${exception.getClass.getName}"""")

      val message = exception.getMessage
      p.message.foreach {
        case Right(a) =>
          Then(s"""STDOUT should include "$a"""")
          message should include(a)
        case Left(a) =>
          Then(s"""STDOUT should not include "$a"""")
          (message should not).include(a)
      }

      p.stdout.foreach {
        case Right(a) =>
          Then(s"""STDOUT should include "$a"""")
          stdout should include(a)
        case Left(a) =>
          Then(s"""STDOUT should not include "$a"""")
          (stdout should not).include(a)
      }

      p.stderr.foreach {
        case Right(a) =>
          Then(s"""STDERR should include "$a"""")
          stderr should include(a)
        case Left(a) =>
          Then(s"""STDERR should not include "$a"""")
          (stderr should not).include(a)
      }

      val stackTraceString = exception.getStackTrace.mkString("\n")
      p.stackTrace.foreach {
        case Left(a) =>
          And(s"""the stack does not include "$a"""")
          (stackTraceString should not).include(a)
        case Right(a) =>
          And(s"""the stack trace includes "$a"""")
          stackTraceString should include(a)
      }

    }
  }

  info("As a Chisel user")
  info("I compile a design")
  Feature("show elaborating message") {
    runStageExpectFiles(
      ChiselMainTest(args = Array("-X", "high"), generator = Some(classOf[SameTypesModule]))
    )
  }

  info("I screw up and compile some bad code")
  Feature("Stack trace trimming of ChiselException") {
    Seq(
      ChiselMainExceptionTest[chisel3.internal.ChiselException](
        args = Array("-X", "low"),
        generator = Some(classOf[DifferentTypesModule]),
        stackTrace = Seq(Left("java"), Right(classOf[DifferentTypesModule].getName))
      ),
      ChiselMainExceptionTest[chisel3.internal.ChiselException](
        args = Array("-X", "low", "--full-stacktrace"),
        generator = Some(classOf[DifferentTypesModule]),
        stackTrace = Seq(Right("java"), Right(classOf[DifferentTypesModule].getName))
      )
    ).foreach(runStageExpectException)
  }
  Feature("Stack trace trimming of user exceptions") {
    Seq(
      ChiselMainExceptionTest[java.lang.IllegalArgumentException](
        args = Array("-X", "low"),
        generator = Some(classOf[FailingRequirementModule]),
        stackTrace = Seq(Right(classOf[FailingRequirementModule].getName), Left("java"))
      ),
      ChiselMainExceptionTest[java.lang.IllegalArgumentException](
        args = Array("-X", "low", "--full-stacktrace"),
        generator = Some(classOf[FailingRequirementModule]),
        stackTrace = Seq(Right(classOf[FailingRequirementModule].getName), Right("java"))
      )
    ).foreach(runStageExpectException)
  }
  Feature("Stack trace trimming and Builder.error errors") {
    Seq(
      ChiselMainExceptionTest[chisel3.internal.ChiselException](
        args = Array("-X", "low"),
        generator = Some(classOf[BuilderErrorModule]),
        message = Seq(Right("Fatal errors during hardware elaboration")),
        stdout = Seq(
          Right(
            "ChiselMainSpec.scala:43: Invalid bit range (3,-1) in class chiselTests.stage.ChiselMainSpec$BuilderErrorModule"
          )
        )
      )
    ).foreach(runStageExpectException)
  }

  Feature("Specifying a custom output file") {
    runStageExpectFiles(
      ChiselMainTest(
        args = Array("--chisel-output-file", "Foo", "--no-run-firrtl"),
        generator = Some(classOf[SameTypesModule]),
        files = Seq("Foo.fir"),
        fileChecks = Map(
          "Foo.fir" -> { file =>
            And("It should be valid FIRRTL")
            Parser.parse(Source.fromFile(file).mkString)
          }
        )
      )
    )
    runStageExpectFiles(
      ChiselMainTest(
        args = Array("--chisel-output-file", "Foo.pb", "--no-run-firrtl"),
        generator = Some(classOf[SameTypesModule]),
        files = Seq("Foo.pb"),
        fileChecks = Map(
          "Foo.pb" -> { file =>
            And("It should be valid ProtoBuf")
            firrtl.proto.FromProto.fromFile(file.toString)
          }
        )
      )
    )
  }

  info("As an aspect writer")
  info("I write an aspect")
  Feature("Running aspects via the command line") {
    Seq(
      ChiselMainTest(
        args = Array("-X", "high", "--with-aspect", "chiselTests.stage.TestClassAspect"),
        generator = Some(classOf[SameTypesModule]),
        stdout = Seq(Right("Ran inspectingAspect"))
      ),
      ChiselMainTest(
        args = Array("-X", "high", "--with-aspect", "chiselTests.stage.TestObjectAspect"),
        generator = Some(classOf[SameTypesModule]),
        stdout = Seq(Right("Ran inspectingAspect"))
      )
    ).foreach(runStageExpectFiles)
  }

}