From 254e7909f6c9d155f514664584f142566f0a6799 Mon Sep 17 00:00:00 2001 From: Schuyler Eldridge Date: Tue, 4 Dec 2018 01:35:48 -0500 Subject: Add tests for Annotations/Options refactor - Add tests for DriverCompatibility.AddImplicitEmitter - Add tests for DriverCompatibility.AddImplicitOutputFile - Use a different top name in DriverSpec emit circuit tests for better coverage - Add tests for DriverCompatibility.WriteEmitted - Add catchWrites firrtlTests utility to intercept file writes - Add tests for WriteOutputAnnotations - Add tests for --custom-transforms reversing Signed-off-by: Schuyler Eldridge --- .../phases/tests/DriverCompatibilitySpec.scala | 219 +++++++++++ src/test/scala/firrtlTests/DriverSpec.scala | 38 +- src/test/scala/firrtlTests/FirrtlSpec.scala | 84 ++++ .../firrtlTests/options/OptionParserSpec.scala | 65 ++-- .../firrtlTests/options/phases/ChecksSpec.scala | 48 +++ .../options/phases/GetIncludesSpec.scala | 96 +++++ .../phases/WriteOutputAnnotationsSpec.scala | 108 ++++++ .../scala/firrtlTests/stage/FirrtlCliSpec.scala | 34 ++ .../scala/firrtlTests/stage/FirrtlMainSpec.scala | 421 +++++++++++++++++++++ .../firrtlTests/stage/FirrtlOptionsViewSpec.scala | 70 ++++ .../firrtlTests/stage/phases/AddCircuitSpec.scala | 101 +++++ .../firrtlTests/stage/phases/AddDefaultsSpec.scala | 37 ++ .../stage/phases/AddImplicitEmitterSpec.scala | 45 +++ .../stage/phases/AddImplicitOutputFileSpec.scala | 46 +++ .../firrtlTests/stage/phases/ChecksSpec.scala | 89 +++++ .../firrtlTests/stage/phases/CompilerSpec.scala | 143 +++++++ .../stage/phases/WriteEmittedSpec.scala | 79 ++++ 17 files changed, 1673 insertions(+), 50 deletions(-) create mode 100644 src/test/scala/firrtl/stage/phases/tests/DriverCompatibilitySpec.scala create mode 100644 src/test/scala/firrtlTests/options/phases/ChecksSpec.scala create mode 100644 src/test/scala/firrtlTests/options/phases/GetIncludesSpec.scala create mode 100644 src/test/scala/firrtlTests/options/phases/WriteOutputAnnotationsSpec.scala create mode 100644 src/test/scala/firrtlTests/stage/FirrtlCliSpec.scala create mode 100644 src/test/scala/firrtlTests/stage/FirrtlMainSpec.scala create mode 100644 src/test/scala/firrtlTests/stage/FirrtlOptionsViewSpec.scala create mode 100644 src/test/scala/firrtlTests/stage/phases/AddCircuitSpec.scala create mode 100644 src/test/scala/firrtlTests/stage/phases/AddDefaultsSpec.scala create mode 100644 src/test/scala/firrtlTests/stage/phases/AddImplicitEmitterSpec.scala create mode 100644 src/test/scala/firrtlTests/stage/phases/AddImplicitOutputFileSpec.scala create mode 100644 src/test/scala/firrtlTests/stage/phases/ChecksSpec.scala create mode 100644 src/test/scala/firrtlTests/stage/phases/CompilerSpec.scala create mode 100644 src/test/scala/firrtlTests/stage/phases/WriteEmittedSpec.scala (limited to 'src') diff --git a/src/test/scala/firrtl/stage/phases/tests/DriverCompatibilitySpec.scala b/src/test/scala/firrtl/stage/phases/tests/DriverCompatibilitySpec.scala new file mode 100644 index 00000000..77316feb --- /dev/null +++ b/src/test/scala/firrtl/stage/phases/tests/DriverCompatibilitySpec.scala @@ -0,0 +1,219 @@ +// See LICENSE for license details. + +package firrtl.stage.phases.tests + +import org.scalatest.{FlatSpec, Matchers, PrivateMethodTester} + +import scala.io.Source + +import java.io.File + +import firrtl._ +import firrtl.stage._ +import firrtl.stage.phases.DriverCompatibility._ + +import firrtl.options.{InputAnnotationFileAnnotation, Phase, TargetDirAnnotation} +import firrtl.stage.phases.DriverCompatibility + +class DriverCompatibilitySpec extends FlatSpec with Matchers with PrivateMethodTester { + + class PhaseFixture(val phase: Phase) + + /* This method wraps some magic that lets you use the private method DriverCompatibility.topName */ + def topName(annotations: AnnotationSeq): Option[String] = { + val topName = PrivateMethod[Option[String]]('topName) + DriverCompatibility invokePrivate topName(annotations) + } + + def simpleCircuit(main: String): String = s"""|circuit $main: + | module $main: + | node x = UInt<1>("h0") + |""".stripMargin + + /* This is a tuple holding an annotation that can be used to derive a top name and the expected top name for that + * annotation. If these annotations are presented here in the same order that DriverCompatibility.topName uses to + * discern a top name. E.g., a TopNameAnnotation is always used, even in the presence of a FirrtlSourceAnnotation. + * Note: the last two FirrtlFileAnnotations have equal precedence, but the first one in the AnnotationSeq wins. + */ + val annosWithTops = Seq( + (TopNameAnnotation("foo"), "foo"), + (FirrtlCircuitAnnotation(Parser.parse(simpleCircuit("bar"))), "bar"), + (FirrtlSourceAnnotation(simpleCircuit("baz")), "baz"), + (FirrtlFileAnnotation("src/test/resources/integration/PipeTester.fir"), "PipeTester"), + (FirrtlFileAnnotation("src/test/resources/integration/GCDTester.pb"), "GCDTester") + ) + + behavior of s"${DriverCompatibility.getClass.getName}.topName (private method)" + + /* This iterates over the tails of annosWithTops. Using the ordering of annosWithTops, if this AnnotationSeq is fed to + * DriverCompatibility.topName, the head annotation will be used to determine the top name. This test ensures that + * topName behaves as expected. + */ + for ( t <- annosWithTops.tails ) t match { + case Nil => + it should "return None on an empty AnnotationSeq" in { + topName(Seq.empty) should be (None) + } + case x => + val annotations = x.map(_._1) + val top = x.head._2 + it should s"determine a top name ('$top') from a ${annotations.head.getClass.getName}" in { + topName(annotations).get should be (top) + } + } + + def createFile(name: String): Unit = { + val file = new File(name) + file.getParentFile.getCanonicalFile.mkdirs() + file.createNewFile() + } + + behavior of classOf[AddImplicitAnnotationFile].toString + + val testDir = "test_run_dir/DriverCompatibilitySpec" + + it should "not modify the annotations if an InputAnnotationFile already exists" in + new PhaseFixture(new AddImplicitAnnotationFile) { + + createFile(testDir + "/foo.anno") + val annotations = Seq( + InputAnnotationFileAnnotation("bar.anno"), + TargetDirAnnotation(testDir), + TopNameAnnotation("foo") ) + + phase.transform(annotations).toSeq should be (annotations) + } + + it should "add an InputAnnotationFile based on a derived topName" in + new PhaseFixture(new AddImplicitAnnotationFile) { + createFile(testDir + "/bar.anno") + val annotations = Seq( + TargetDirAnnotation(testDir), + TopNameAnnotation("bar") ) + + val expected = annotations.toSet + + InputAnnotationFileAnnotation(testDir + "/bar.anno") + + phase.transform(annotations).toSet should be (expected) + } + + it should "not add an InputAnnotationFile for .anno.json annotations" in + new PhaseFixture(new AddImplicitAnnotationFile) { + createFile(testDir + "/baz.anno.json") + val annotations = Seq( + TargetDirAnnotation(testDir), + TopNameAnnotation("baz") ) + + phase.transform(annotations).toSeq should be (annotations) + } + + it should "not add an InputAnnotationFile if it cannot determine the topName" in + new PhaseFixture(new AddImplicitAnnotationFile) { + val annotations = Seq( TargetDirAnnotation(testDir) ) + + phase.transform(annotations).toSeq should be (annotations) + } + + behavior of classOf[AddImplicitFirrtlFile].toString + + it should "not modify the annotations if a CircuitOption is present" in + new PhaseFixture(new AddImplicitFirrtlFile) { + val annotations = Seq( FirrtlFileAnnotation("foo"), TopNameAnnotation("bar") ) + + phase.transform(annotations).toSeq should be (annotations) + } + + it should "add an FirrtlFileAnnotation if a TopNameAnnotation is present" in + new PhaseFixture(new AddImplicitFirrtlFile) { + val annotations = Seq( TopNameAnnotation("foo") ) + val expected = annotations.toSet + + FirrtlFileAnnotation(new File("foo.fir").getCanonicalPath) + + phase.transform(annotations).toSet should be (expected) + } + + it should "do nothing if no TopNameAnnotation is present" in + new PhaseFixture(new AddImplicitFirrtlFile) { + val annotations = Seq( TargetDirAnnotation("foo") ) + + phase.transform(annotations).toSeq should be (annotations) + } + + behavior of classOf[AddImplicitEmitter].toString + + val (nc, hfc, mfc, lfc, vc, svc) = ( new NoneCompiler, + new HighFirrtlCompiler, + new MiddleFirrtlCompiler, + new LowFirrtlCompiler, + new VerilogCompiler, + new SystemVerilogCompiler ) + + it should "convert CompilerAnnotations into EmitCircuitAnnotations without EmitOneFilePerModuleAnnotation" in + new PhaseFixture(new AddImplicitEmitter) { + val annotations = Seq( + CompilerAnnotation(nc), + CompilerAnnotation(hfc), + CompilerAnnotation(mfc), + CompilerAnnotation(lfc), + CompilerAnnotation(vc), + CompilerAnnotation(svc) + ) + val expected = annotations + .flatMap( a => Seq(a, + RunFirrtlTransformAnnotation(a.compiler.emitter), + EmitCircuitAnnotation(a.compiler.emitter.getClass)) ) + + phase.transform(annotations).toSeq should be (expected) + } + + it should "convert CompilerAnnotations into EmitAllodulesAnnotation with EmitOneFilePerModuleAnnotation" in + new PhaseFixture(new AddImplicitEmitter) { + val annotations = Seq( + EmitOneFilePerModuleAnnotation, + CompilerAnnotation(nc), + CompilerAnnotation(hfc), + CompilerAnnotation(mfc), + CompilerAnnotation(lfc), + CompilerAnnotation(vc), + CompilerAnnotation(svc) + ) + val expected = annotations + .flatMap{ + case a: CompilerAnnotation => Seq(a, + RunFirrtlTransformAnnotation(a.compiler.emitter), + EmitAllModulesAnnotation(a.compiler.emitter.getClass)) + case a => Seq(a) + } + + phase.transform(annotations).toSeq should be (expected) + } + + behavior of classOf[AddImplicitOutputFile].toString + + it should "add an OutputFileAnnotation derived from a TopNameAnnotation if no OutputFileAnnotation exists" in + new PhaseFixture(new AddImplicitOutputFile) { + val annotations = Seq( TopNameAnnotation("foo") ) + val expected = Seq( + OutputFileAnnotation("foo"), + TopNameAnnotation("foo") + ) + phase.transform(annotations).toSeq should be (expected) + } + + it should "do nothing if an OutputFileannotation already exists" in + new PhaseFixture(new AddImplicitOutputFile) { + val annotations = Seq( + TopNameAnnotation("foo"), + OutputFileAnnotation("bar") ) + val expected = annotations + phase.transform(annotations).toSeq should be (expected) + } + + it should "do nothing if no TopNameAnnotation exists" in + new PhaseFixture(new AddImplicitOutputFile) { + val annotations = Seq.empty + val expected = annotations + phase.transform(annotations).toSeq should be (expected) + } + +} diff --git a/src/test/scala/firrtlTests/DriverSpec.scala b/src/test/scala/firrtlTests/DriverSpec.scala index d26aadf2..1cb991c8 100644 --- a/src/test/scala/firrtlTests/DriverSpec.scala +++ b/src/test/scala/firrtlTests/DriverSpec.scala @@ -218,6 +218,7 @@ class DriverSpec extends FreeSpec with Matchers with BackendCompilationUtilities firrtlOptions = firrtlOptions.copy(firrtlSource = Some(input)) } val annoFile = new File(optionsManager.commonOptions.targetDirName, top + ".anno") + val vFile = new File(optionsManager.commonOptions.targetDirName, top + ".v") "Using Driver.getAnnotations" in { copyResourceToFile("/annotations/SampleAnnotations.anno", annoFile) optionsManager.firrtlOptions.annotations.length should be(0) @@ -225,6 +226,7 @@ class DriverSpec extends FreeSpec with Matchers with BackendCompilationUtilities annos.length should be(12) // 9 from circuit plus 3 general purpose annos.count(_.isInstanceOf[InlineAnnotation]) should be(9) annoFile.delete() + vFile.delete() } "Using Driver.execute" in { copyResourceToFile("/annotations/SampleAnnotations.anno", annoFile) @@ -234,6 +236,7 @@ class DriverSpec extends FreeSpec with Matchers with BackendCompilationUtilities annos.count(_.isInstanceOf[InlineAnnotation]) should be(9) } annoFile.delete() + vFile.delete() } } @@ -384,20 +387,29 @@ class DriverSpec extends FreeSpec with Matchers with BackendCompilationUtilities "To a single file with file extension depending on the compiler by default" in { Seq( - "none" -> "./Top.fir", - "low" -> "./Top.lo.fir", - "high" -> "./Top.hi.fir", - "middle" -> "./Top.mid.fir", - "verilog" -> "./Top.v", - "mverilog" -> "./Top.v", - "sverilog" -> "./Top.sv" + "none" -> "./Foo.fir", + "low" -> "./Foo.lo.fir", + "high" -> "./Foo.hi.fir", + "middle" -> "./Foo.mid.fir", + "verilog" -> "./Foo.v", + "mverilog" -> "./Foo.v", + "sverilog" -> "./Foo.sv" ).foreach { case (compilerName, expectedOutputFileName) => + info(s"$compilerName -> $expectedOutputFileName") val manager = new ExecutionOptionsManager("test") with HasFirrtlOptions { - commonOptions = CommonOptions(topName = "Top") + commonOptions = CommonOptions(topName = "Foo") firrtlOptions = FirrtlExecutionOptions(firrtlSource = Some(input), compilerName = compilerName) } - firrtl.Driver.execute(manager) + firrtl.Driver.execute(manager) match { + case success: FirrtlExecutionSuccess => + success.emitted.size should not be (0) + success.circuitState.annotations.length should be > (0) + case a: FirrtlExecutionFailure => + fail(s"Got a FirrtlExecutionFailure! Expected FirrtlExecutionSuccess. Full message:\n${a.message}") + } + + val file = new File(expectedOutputFileName) file.exists() should be(true) @@ -414,9 +426,8 @@ class DriverSpec extends FreeSpec with Matchers with BackendCompilationUtilities "mverilog" -> Seq("./Top.v", "./Child.v"), "sverilog" -> Seq("./Top.sv", "./Child.sv") ).foreach { case (compilerName, expectedOutputFileNames) => - println(s"$compilerName -> $expectedOutputFileNames") + info(s"$compilerName -> $expectedOutputFileNames") val manager = new ExecutionOptionsManager("test") with HasFirrtlOptions { - commonOptions = CommonOptions(topName = "Top") firrtlOptions = FirrtlExecutionOptions(firrtlSource = Some(input), compilerName = compilerName, emitOneFilePerModule = true) @@ -424,9 +435,10 @@ class DriverSpec extends FreeSpec with Matchers with BackendCompilationUtilities firrtl.Driver.execute(manager) match { case success: FirrtlExecutionSuccess => + success.emitted.size should not be (0) success.circuitState.annotations.length should be > (0) - case _ => - + case failure: FirrtlExecutionFailure => + fail(s"Got a FirrtlExecutionFailure! Expected FirrtlExecutionSuccess. Full message:\n${failure.message}") } for (name <- expectedOutputFileNames) { diff --git a/src/test/scala/firrtlTests/FirrtlSpec.scala b/src/test/scala/firrtlTests/FirrtlSpec.scala index c3c25cd5..c110109c 100644 --- a/src/test/scala/firrtlTests/FirrtlSpec.scala +++ b/src/test/scala/firrtlTests/FirrtlSpec.scala @@ -3,6 +3,7 @@ package firrtlTests import java.io._ +import java.security.Permission import com.typesafe.scalalogging.LazyLogging @@ -320,3 +321,86 @@ abstract class CompilationTest(name: String, dir: String) extends FirrtlPropSpec compileFirrtlTest(name, dir) } } + +trait Utils { + + /** Run some Scala thunk and return STDOUT and STDERR as strings. + * @param thunk some Scala code + * @return a tuple containing STDOUT, STDERR, and what the thunk returns + */ + def grabStdOutErr[T](thunk: => T): (String, String, T) = { + val stdout, stderr = new ByteArrayOutputStream() + val ret = scala.Console.withOut(stdout) { scala.Console.withErr(stderr) { thunk } } + (stdout.toString, stderr.toString, ret) + } + + /** Encodes a System.exit exit code + * @param status the exit code + */ + private case class ExitException(status: Int) extends SecurityException(s"Found a sys.exit with code $status") + + /** A security manager that converts calls to System.exit into [[ExitException]]s by explicitly disabling the ability of + * a thread to actually exit. For more information, see: + * - https://docs.oracle.com/javase/tutorial/essential/environment/security.html + */ + private class ExceptOnExit extends SecurityManager { + override def checkPermission(perm: Permission): Unit = {} + override def checkPermission(perm: Permission, context: Object): Unit = {} + override def checkExit(status: Int): Unit = { + super.checkExit(status) + throw ExitException(status) + } + } + + /** Encodes a file that some code tries to write to + * @param the file name + */ + private case class WriteException(file: String) extends SecurityException(s"Tried to write to file $file") + + /** A security manager that converts writes to any file into [[WriteException]]s. + */ + private class ExceptOnWrite extends SecurityManager { + override def checkPermission(perm: Permission): Unit = {} + override def checkPermission(perm: Permission, context: Object): Unit = {} + override def checkWrite(file: String): Unit = { + super.checkWrite(file) + throw WriteException(file) + } + } + + /** Run some Scala code (a thunk) in an environment where all System.exit are caught and returned. This avoids a + * situation where a test results in something actually exiting and killing the entire test. This is necessary if you + * want to test a command line program, e.g., the `main` method of [[firrtl.options.Stage Stage]]. + * + * NOTE: THIS WILL NOT WORK IN SITUATIONS WHERE THE THUNK IS CATCHING ALL [[Exception]]s OR [[Throwable]]s, E.G., + * SCOPT. IF THIS IS HAPPENING THIS WILL NOT WORK. REPEAT THIS WILL NOT WORK. + * @param thunk some Scala code + * @return either the output of the thunk (`Right[T]`) or an exit code (`Left[Int]`) + */ + def catchStatus[T](thunk: => T): Either[Int, T] = { + try { + System.setSecurityManager(new ExceptOnExit()) + Right(thunk) + } catch { + case ExitException(a) => Left(a) + } finally { + System.setSecurityManager(null) + } + } + + /** Run some Scala code (a thunk) in an environment where file writes are caught and the file that a program tries to + * write to is returned. This is useful if you want to test that some thunk either tries to write to a specific file + * or doesn't try to write at all. + */ + def catchWrites[T](thunk: => T): Either[String, T] = { + try { + System.setSecurityManager(new ExceptOnWrite()) + Right(thunk) + } catch { + case WriteException(a) => Left(a) + } finally { + System.setSecurityManager(null) + } + } + +} diff --git a/src/test/scala/firrtlTests/options/OptionParserSpec.scala b/src/test/scala/firrtlTests/options/OptionParserSpec.scala index ae4899d4..0a9ac2d5 100644 --- a/src/test/scala/firrtlTests/options/OptionParserSpec.scala +++ b/src/test/scala/firrtlTests/options/OptionParserSpec.scala @@ -4,15 +4,13 @@ package firrtlTests.options import firrtl.{AnnotationSeq, FIRRTLException} import firrtl.annotations.{Annotation, NoTargetAnnotation} -import firrtl.options.{DoNotTerminateOnExit, DuplicateHandling, OptionsException} +import firrtl.options.{DoNotTerminateOnExit, DuplicateHandling, ExceptOnError, OptionsException} import scopt.OptionParser import org.scalatest.{FlatSpec, Matchers} -import java.security.Permission - -class OptionParserSpec extends FlatSpec with Matchers { +class OptionParserSpec extends FlatSpec with Matchers with firrtlTests.Utils { case class IntAnnotation(x: Int) extends NoTargetAnnotation { def extract: Int = x @@ -32,50 +30,35 @@ class OptionParserSpec extends FlatSpec with Matchers { opt[Int]("integer").abbr("m").unbounded.action( (x, c) => IntAnnotation(x) +: c ) } - case class ExitException(status: Option[Int]) extends SecurityException("Found a sys.exit") + trait WithIntParser { val parser = new IntParser } - /* Security manager that disallows calls to sys.exit */ - class ExceptOnExit extends SecurityManager { - override def checkPermission(perm: Permission): Unit = {} - override def checkPermission(perm: Permission, context: Object): Unit = {} - override def checkExit(status: Int): Unit = { - super.checkExit(status) - throw ExitException(Some(status)) - } - } + behavior of "A default OptionsParser" - /* Tell a parser to terminate in an environment where sys.exit throws an exception */ - def catchStatus(parser: OptionParser[_], exitState: Either[String, Unit]): Option[Int] = { - System.setSecurityManager(new ExceptOnExit()) - val status = try { - parser.terminate(exitState) - throw new ExitException(None) - } catch { - case ExitException(s) => s - } - System.setSecurityManager(null) - status - } + it should "call sys.exit if terminate is called" in new WithIntParser { + info("exit status of 1 for failure") + catchStatus { parser.terminate(Left("some message")) } should be (Left(1)) - behavior of "default OptionsParser" - - it should "terminate on exit" in { - val parser = new IntParser + info("exit status of 0 for success") + catchStatus { parser.terminate(Right(Unit)) } should be (Left(0)) + } - info("By default, exit statuses are reported") - catchStatus(parser, Left("some message")) should be (Some(1)) - catchStatus(parser, Right(Unit)) should be (Some(0)) + it should "print to stderr on an invalid option" in new WithIntParser { + grabStdOutErr{ parser.parse(Array("--foo"), Seq[Annotation]()) }._2 should include ("Unknown option --foo") } - behavior of "DoNotTerminateOnExit" + behavior of "An OptionParser with DoNotTerminateOnExit mixed in" it should "disable sys.exit for terminate method" in { val parser = new IntParser with DoNotTerminateOnExit - catchStatus(parser, Left("some message")) should be (None) - catchStatus(parser, Right(Unit)) should be (None) + + info("no exit for failure") + catchStatus { parser.terminate(Left("some message")) } should be (Right(())) + + info("no exit for success") + catchStatus { parser.terminate(Right(Unit)) } should be (Right(())) } - behavior of "DuplicateHandling" + behavior of "An OptionParser with DuplicateHandling mixed in" it should "detect short duplicates" in { val parser = new IntParser with DuplicateHandling with DuplicateShortOption @@ -89,4 +72,12 @@ class OptionParserSpec extends FlatSpec with Matchers { .getMessage should startWith ("Duplicate long option") } + behavior of "An OptionParser with ExceptOnError mixed in" + + it should "cause an OptionsException on an invalid option" in { + val parser = new IntParser with ExceptOnError + intercept[OptionsException] { parser.parse(Array("--foo"), Seq[Annotation]()) } + .getMessage should include ("Unknown option") + } + } diff --git a/src/test/scala/firrtlTests/options/phases/ChecksSpec.scala b/src/test/scala/firrtlTests/options/phases/ChecksSpec.scala new file mode 100644 index 00000000..e04ba554 --- /dev/null +++ b/src/test/scala/firrtlTests/options/phases/ChecksSpec.scala @@ -0,0 +1,48 @@ +// See LICENSE for license details. + +package firrtlTests.options.phases + +import org.scalatest.{FlatSpec, Matchers} + +import firrtl.AnnotationSeq +import firrtl.options.{OptionsException, OutputAnnotationFileAnnotation, Phase, TargetDirAnnotation} +import firrtl.options.phases.Checks + +class ChecksSpec extends FlatSpec with Matchers { + + val targetDir = TargetDirAnnotation("foo") + val annoOut = OutputAnnotationFileAnnotation("bar") + + /* A minimum annotation Seq that will pass [[Checks]] */ + val min = Seq(targetDir) + + def checkExceptionMessage(phase: Phase, annotations: AnnotationSeq, messageStart: String): Unit = + intercept[OptionsException]{ phase.transform(annotations) }.getMessage should startWith(messageStart) + + class Fixture { val phase: Phase = new Checks } + + behavior of classOf[Checks].toString + + it should "enforce exactly one TargetDirAnnotation" in new Fixture { + info("0 target directories throws an exception") + checkExceptionMessage(phase, Seq.empty, "Exactly one target directory must be specified") + + info("2 target directories throws an exception") + checkExceptionMessage(phase, Seq(targetDir, targetDir), "Exactly one target directory must be specified") + } + + it should "enforce zero or one output annotation files" in new Fixture { + info("0 output annotation files is okay") + phase.transform(Seq(targetDir)) + + info("2 output annotation files throws an exception") + val in = Seq(targetDir, annoOut, annoOut) + checkExceptionMessage(phase, in, "At most one output annotation file can be specified") + } + + it should "pass if the minimum annotations are specified" in new Fixture { + info(s"""Minimum required: ${min.map(_.getClass.getSimpleName).mkString(", ")}""") + phase.transform(min) + } + +} diff --git a/src/test/scala/firrtlTests/options/phases/GetIncludesSpec.scala b/src/test/scala/firrtlTests/options/phases/GetIncludesSpec.scala new file mode 100644 index 00000000..5b07d0e0 --- /dev/null +++ b/src/test/scala/firrtlTests/options/phases/GetIncludesSpec.scala @@ -0,0 +1,96 @@ +// See LICENSE for license details. + +package firrtlTests.options.phases + +import org.scalatest.{FlatSpec, Matchers} + +import java.io.{File, PrintWriter} + +import firrtl.AnnotationSeq +import firrtl.annotations.{Annotation, AnnotationFileNotFoundException, JsonProtocol, + NoTargetAnnotation} +import firrtl.options.phases.GetIncludes +import firrtl.options.{InputAnnotationFileAnnotation, Phase} +import firrtl.util.BackendCompilationUtilities + +case object A extends NoTargetAnnotation +case object B extends NoTargetAnnotation +case object C extends NoTargetAnnotation +case object D extends NoTargetAnnotation +case object E extends NoTargetAnnotation + +class GetIncludesSpec extends FlatSpec with Matchers with BackendCompilationUtilities with firrtlTests.Utils { + + + val dir = new File("test_run_dir/GetIncludesSpec") + dir.mkdirs() + + def ref(filename: String): InputAnnotationFileAnnotation = InputAnnotationFileAnnotation(s"$dir/$filename.anno.json") + + def checkAnnos(a: AnnotationSeq, b: AnnotationSeq): Unit = { + info("read the expected number of annotations") + a.size should be (b.size) + + info("annotations match exact order") + a.zip(b).foreach{ case (ax, bx) => ax should be (bx) } + } + + val files = Seq( + new File(dir + "/a.anno.json") -> Seq(A, ref("b")), + new File(dir + "/b.anno.json") -> Seq(B, ref("c"), ref("a")), + new File(dir + "/c.anno.json") -> Seq(C, ref("d"), ref("e")), + new File(dir + "/d.anno.json") -> Seq(D), + new File(dir + "/e.anno.json") -> Seq(E) + ) + + files.foreach{ case (file, annotations) => + val pw = new PrintWriter(file) + pw.write(JsonProtocol.serialize(annotations)) + pw.close() + } + + class Fixture { val phase: Phase = new GetIncludes } + + behavior of classOf[GetIncludes].toString + + it should "throw an exception if the annotation file doesn't exit" in new Fixture { + intercept[AnnotationFileNotFoundException]{ phase.transform(Seq(ref("f"))) } + .getMessage should startWith("Annotation file") + } + + it should "read annotations from a file" in new Fixture { + val e = ref("e") + val in = Seq(e) + val expect = Seq(E) + val out = phase.transform(in) + + checkAnnos(out, expect) + } + + it should "read annotations from multiple files, but not reading duplicates" in new Fixture { + val Seq(d, e) = Seq("d", "e").map(ref) + val in = Seq(d, e, e, d) + val expect = Seq(D, E) + val (stdout, _, out) = grabStdOutErr { phase.transform(in) } + + checkAnnos(out, expect) + + Seq("d", "e").foreach{ x => + info(s"a warning about '$x.anno.json' was printed") + stdout should include (s"Warning: Annotation file ($dir/$x.anno.json) already included!") + } + } + + it should "handle recursive references gracefully, but show a warning" in new Fixture { + val Seq(a, b, c, d, e) = Seq("a", "b", "c", "d", "e").map(ref) + val in = Seq(a) + val expect = Seq(A, B, C, D, E) + val (stdout, _, out) = grabStdOutErr { phase.transform(in) } + + checkAnnos(out, expect) + + info("a warning about 'a.anno.json' was printed") + stdout should include (s"Warning: Annotation file ($dir/a.anno.json)") + } + +} diff --git a/src/test/scala/firrtlTests/options/phases/WriteOutputAnnotationsSpec.scala b/src/test/scala/firrtlTests/options/phases/WriteOutputAnnotationsSpec.scala new file mode 100644 index 00000000..07aad53f --- /dev/null +++ b/src/test/scala/firrtlTests/options/phases/WriteOutputAnnotationsSpec.scala @@ -0,0 +1,108 @@ +// See LICENSE for license details. + +package firrtlTests.options.phases + +import org.scalatest.{FlatSpec, Matchers} + +import java.io.File + +import firrtl.{AnnotationSeq, EmittedFirrtlCircuitAnnotation, EmittedFirrtlCircuit} +import firrtl.annotations.{DeletedAnnotation, NoTargetAnnotation} +import firrtl.options.{InputAnnotationFileAnnotation, OutputAnnotationFileAnnotation, Phase, WriteDeletedAnnotation} +import firrtl.options.phases.{GetIncludes, WriteOutputAnnotations} +import firrtl.stage.FirrtlFileAnnotation + +class WriteOutputAnnotationsSpec extends FlatSpec with Matchers with firrtlTests.Utils { + + val dir = "test_run_dir/WriteOutputAnnotationSpec" + + /** Check if the annotations contained by a [[File]] and the same, and in the same order, as a reference + * [[AnnotationSeq]]. This uses [[GetIncludes]] as that already knows how to read annotation files. + * @param f a file to read + * @param a the expected annotations + */ + private def fileContainsAnnotations(f: File, a: AnnotationSeq): Unit = { + info(s"output file '$f' exists") + f should (exist) + + info(s"reading '$f' works") + val read = (new GetIncludes) + .transform(Seq(InputAnnotationFileAnnotation(f.toString))) + .filterNot{ + case a @ DeletedAnnotation(_, _: InputAnnotationFileAnnotation) => true + case _ => false } + + info(s"annotations in file are expected size") + read.size should be (a.size) + + read + .zip(a) + .foreach{ case (read, expected) => + info(s"$read matches") + read should be (expected) } + + f.delete() + } + + class Fixture { val phase: Phase = new WriteOutputAnnotations } + + behavior of classOf[WriteOutputAnnotations].toString + + it should "write annotations to a file (excluding DeletedAnnotations)" in new Fixture { + val file = new File(dir + "/should-write-annotations-to-a-file.anno.json") + val annotations = Seq( OutputAnnotationFileAnnotation(file.toString), + WriteOutputAnnotationsSpec.FooAnnotation, + WriteOutputAnnotationsSpec.BarAnnotation(0), + WriteOutputAnnotationsSpec.BarAnnotation(1), + DeletedAnnotation("foo", WriteOutputAnnotationsSpec.FooAnnotation) ) + val expected = annotations.filter { + case a: DeletedAnnotation => false + case a => true + } + val out = phase.transform(annotations) + + info("annotations are unmodified") + out.toSeq should be (annotations) + + fileContainsAnnotations(file, expected) + } + + it should "include DeletedAnnotations if a WriteDeletedAnnotation is present" in new Fixture { + val file = new File(dir + "should-include-deleted.anno.json") + val annotations = Seq( OutputAnnotationFileAnnotation(file.toString), + WriteOutputAnnotationsSpec.FooAnnotation, + WriteOutputAnnotationsSpec.BarAnnotation(0), + WriteOutputAnnotationsSpec.BarAnnotation(1), + DeletedAnnotation("foo", WriteOutputAnnotationsSpec.FooAnnotation), + WriteDeletedAnnotation ) + val out = phase.transform(annotations) + + info("annotations are unmodified") + out.toSeq should be (annotations) + + fileContainsAnnotations(file, annotations) + } + + it should "do nothing if no output annotation file is specified" in new Fixture { + val annotations = Seq( WriteOutputAnnotationsSpec.FooAnnotation, + WriteOutputAnnotationsSpec.BarAnnotation(0), + WriteOutputAnnotationsSpec.BarAnnotation(1) ) + + val out = catchWrites { phase.transform(annotations) } match { + case Right(a) => + info("no file writes occurred") + a + case Left(a) => + fail(s"No file writes expected, but a write to '$a' ocurred!") + } + + info("annotations are unmodified") + out.toSeq should be (annotations) + } + +} + +private object WriteOutputAnnotationsSpec { + case object FooAnnotation extends NoTargetAnnotation + case class BarAnnotation(x: Int) extends NoTargetAnnotation +} diff --git a/src/test/scala/firrtlTests/stage/FirrtlCliSpec.scala b/src/test/scala/firrtlTests/stage/FirrtlCliSpec.scala new file mode 100644 index 00000000..f13a498f --- /dev/null +++ b/src/test/scala/firrtlTests/stage/FirrtlCliSpec.scala @@ -0,0 +1,34 @@ +// See LICENSE for license details. + +package firrtlTests.stage + +import org.scalatest.{FlatSpec, Matchers} + +import firrtl.stage.RunFirrtlTransformAnnotation +import firrtl.options.Shell +import firrtl.stage.FirrtlCli + +class FirrtlCliSpec extends FlatSpec with Matchers { + + behavior of "FirrtlCli for RunFirrtlTransformAnnotation / -fct / --custom-transforms" + + it should "preserver transform order" in { + val shell = new Shell("foo") with FirrtlCli + val args = Array( + "--custom-transforms", "firrtl.transforms.BlackBoxSourceHelper,firrtl.transforms.CheckCombLoops", + "--custom-transforms", "firrtl.transforms.CombineCats", + "--custom-transforms", "firrtl.transforms.ConstantPropagation" ) + val expected = Seq( + classOf[firrtl.transforms.BlackBoxSourceHelper], + classOf[firrtl.transforms.CheckCombLoops], + classOf[firrtl.transforms.CombineCats], + classOf[firrtl.transforms.ConstantPropagation] ) + + shell + .parse(args) + .collect{ case a: RunFirrtlTransformAnnotation => a } + .zip(expected) + .map{ case (RunFirrtlTransformAnnotation(a), b) => a.getClass should be (b) } + } + +} diff --git a/src/test/scala/firrtlTests/stage/FirrtlMainSpec.scala b/src/test/scala/firrtlTests/stage/FirrtlMainSpec.scala new file mode 100644 index 00000000..ca766d52 --- /dev/null +++ b/src/test/scala/firrtlTests/stage/FirrtlMainSpec.scala @@ -0,0 +1,421 @@ +// See LICENSE for license details. + +package firrtlTests.stage + +import org.scalatest.{FeatureSpec, GivenWhenThen, Matchers} + +import java.io.{File, PrintWriter} + +import scala.io.Source + +import firrtl.stage.FirrtlMain +import firrtl.util.BackendCompilationUtilities + +/** Testing for the top-level [[FirrtlStage]] via [[FirrtlMain]]. + * + * This test uses the [[org.scalatest.FeatureSpec FeatureSpec]] intentionally as this test exercises the top-level + * interface and is more suitable to an Acceptance Testing style. + */ +class FirrtlMainSpec extends FeatureSpec with GivenWhenThen with Matchers with firrtlTests.Utils + with BackendCompilationUtilities { + + /** Parameterizes one test of [[FirrtlMain]]. Running the [[FirrtlMain]] `main` with certain args should produce + * certain files. + * @param args arguments to pass + * @param circuit a [[FirrtlCircuitFixture]] to use. This will generate an appropriate '-i $targetDir/$main.fi' + * argument. + * @param files expected files that will be created + * @param stdout expected stdout string, None if no output expected + * @param stderr expected stderr string, None if no output expected + * @param result expected exit code + */ + case class FirrtlMainTest( + args: Array[String], + circuit: Option[FirrtlCircuitFixture] = Some(new SimpleFirrtlCircuitFixture), + files: Seq[String] = Seq.empty, + stdout: Option[String] = None, + stderr: Option[String] = None, + result: Int = 0) { + /** Generate a name for the test based on the arguments */ + def testName: String = "args" + args.mkString("_") + + /** Print the arguments as a single string */ + def argsString: String = args.mkString(" ") + } + + /** Run the FIRRTL stage with some command line arguments expecting output files to be created. The target directory is + * implied, but will be created by the stage. + * @param p some test parameters + */ + def runStageExpectFiles(p: FirrtlMainTest): Unit = { + scenario(s"""User runs FIRRTL Stage with '${p.argsString}'""") { + val f = new FirrtlMainFixture + val td = new TargetDirectoryFixture(p.testName) + + val inputFile: Array[String] = p.circuit match { + case Some(c) => + And("some input FIRRTL IR") + val in = new File(td.dir, c.main) + val pw = new PrintWriter(in) + pw.write(c.input) + pw.close() + Array("-i", in.toString) + case None => Array.empty + } + + p.files.foreach( f => new File(td.buildDir + s"/$f").delete() ) + + When(s"""the user tries to compile with '${p.argsString}'""") + val (stdout, stderr, result) = + grabStdOutErr { catchStatus { f.stage.main(inputFile ++ Array("-td", td.buildDir.toString) ++ p.args) } } + + p.stdout match { + case Some(a) => + Then(s"""STDOUT should include "$a"""") + stdout should include (a) + case None => + Then(s"nothing should print to STDOUT") + stdout should be (empty) + } + + p.stderr match { + case Some(a) => + And(s"""STDERR should include "$a"""") + stderr should include (a) + case None => + And(s"nothing should print to STDERR") + stderr should be (empty) + } + + 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) + } + } + } + + + /** Test fixture that links to the [[FirrtlMain]] object. This could be done without, but its use matches the + * Given/When/Then style more accurately. + */ + class FirrtlMainFixture { + Given("the FIRRTL stage") + val stage = FirrtlMain + } + + /** Test fixture that creates a build directory + * @param dirName the name of the base directory; a `build` directory is created under this + */ + class TargetDirectoryFixture(dirName: String) { + val dir = new File(s"test_run_dir/FirrtlMainSpec/$dirName") + val buildDir = new File(dir + "/build") + dir.mkdirs() + } + + trait FirrtlCircuitFixture { + val main: String + val input: String + } + + /** Test fixture defining a simple FIRRTL circuit that will emit differently with and without `--split-modules`. */ + class SimpleFirrtlCircuitFixture extends FirrtlCircuitFixture { + val main: String = "Top" + val input: String = + """|circuit Top: + | module Top: + | output foo: UInt<32> + | inst c of Child + | inst e of External + | foo <= tail(add(c.foo, e.foo), 1) + | module Child: + | output foo: UInt<32> + | inst e of External + | foo <= e.foo + | extmodule External: + | output foo: UInt<32> + |""".stripMargin + } + + info("As a FIRRTL command line user") + info("I want to compile some FIRRTL") + feature("FirrtlMain command line interface") { + scenario("User tries to discover available options") { + val f = new FirrtlMainFixture + + When("the user passes '--help'") + /* Note: THIS CANNOT CATCH THE STATUS BECAUSE SCOPT CATCHES ALL THROWABLE!!! The catchStatus is only used to prevent + * sys.exit from killing the test. However, this should additionally return an exit code of 0 and not print an + * error. The nature of running this through catchStatus causes scopt to intercept the custom SecurityException + * and then use that as evidence to exit with a code of 1. + */ + val (out, _, result) = grabStdOutErr { catchStatus { f.stage.main(Array("--help")) } } + + Then("the usage text should be shown") + out should include ("Usage: firrtl") + + And("usage text should show known registered transforms") + out should include ("--no-dce") + + And("usage text should show known registered libraries") + out should include ("MemLib Options") + + info("""And the exit code should be 0, but scopt catches all throwable, so we can't check this... ¯\_(ツ)_/¯""") + // And("the exit code should be zero") + // out should be (Left(0)) + } + + Seq( + /* Test all standard emitters with and without annotation file outputs */ + FirrtlMainTest(args = Array("-X", "none", "-E", "chirrtl"), + files = Seq("Top.fir")), + FirrtlMainTest(args = Array("-X", "high", "-E", "high"), + files = Seq("Top.hi.fir")), + FirrtlMainTest(args = Array("-X", "middle", "-E", "middle", "-foaf", "Top"), + files = Seq("Top.mid.fir", "Top.anno.json")), + FirrtlMainTest(args = Array("-X", "low", "-E", "low", "-foaf", "annotations.anno.json"), + files = Seq("Top.lo.fir", "annotations.anno.json")), + FirrtlMainTest(args = Array("-X", "verilog", "-E", "verilog", "-foaf", "foo.anno"), + files = Seq("Top.v", "foo.anno.anno.json")), + FirrtlMainTest(args = Array("-X", "sverilog", "-E", "sverilog", "-foaf", "foo.json"), + files = Seq("Top.sv", "foo.json.anno.json"), + stdout = Some("SystemVerilog Compiler behaves the same as the Verilog Compiler!")), + + /* Test all one file per module emitters */ + FirrtlMainTest(args = Array("-X", "none", "-e", "chirrtl"), + files = Seq("Top.fir", "Child.fir")), + FirrtlMainTest(args = Array("-X", "high", "-e", "high"), + files = Seq("Top.hi.fir", "Child.hi.fir")), + FirrtlMainTest(args = Array("-X", "middle", "-e", "middle"), + files = Seq("Top.mid.fir", "Child.mid.fir")), + FirrtlMainTest(args = Array("-X", "low", "-e", "low"), + files = Seq("Top.lo.fir", "Child.lo.fir")), + FirrtlMainTest(args = Array("-X", "verilog", "-e", "verilog"), + files = Seq("Top.v", "Child.v")), + FirrtlMainTest(args = Array("-X", "sverilog", "-e", "sverilog"), + files = Seq("Top.sv", "Child.sv"), + stdout = Some("SystemVerilog Compiler behaves the same as the Verilog Compiler!")), + + /* Test changes to output file name */ + FirrtlMainTest(args = Array("-X", "none", "-E", "chirrtl", "-o", "foo"), + files = Seq("foo.fir")), + FirrtlMainTest(args = Array("-X", "high", "-E", "high", "-o", "foo"), + files = Seq("foo.hi.fir")), + FirrtlMainTest(args = Array("-X", "middle", "-E", "middle", "-o", "foo.middle"), + files = Seq("foo.middle.mid.fir")), + FirrtlMainTest(args = Array("-X", "low", "-E", "low", "-o", "foo.lo.fir"), + files = Seq("foo.lo.fir")), + FirrtlMainTest(args = Array("-X", "verilog", "-E", "verilog", "-o", "foo.sv"), + files = Seq("foo.sv.v")), + FirrtlMainTest(args = Array("-X", "sverilog", "-E", "sverilog", "-o", "Foo"), + files = Seq("Foo.sv"), + stdout = Some("SystemVerilog Compiler behaves the same as the Verilog Compiler!")) + ) + .foreach(runStageExpectFiles) + + scenario("User doesn't specify a target directory") { + val f = new FirrtlMainFixture + + When("the user doesn't specify a target directory") + val outName = "FirrtlMainSpecNoTargetDirectory" + val out = new File(s"$outName.hi.fir") + out.delete() + val result = catchStatus { + f.stage.main(Array("-i", "src/test/resources/integration/GCDTester.fir", "-o", outName, "-X", "high", + "-E", "high")) } + + Then("outputs should be written to current directory") + out should (exist) + out.delete() + } + + scenario("User provides Protocol Buffer input") { + val f = new FirrtlMainFixture + val td = new TargetDirectoryFixture("protobuf-works") + + And("some Protocol Buffer input") + val protobufIn = new File(td.dir + "/Foo.pb") + copyResourceToFile("/integration/GCDTester.pb", protobufIn) + + When("the user tries to compile to High FIRRTL") + f.stage.main(Array("-i", protobufIn.toString, "-X", "high", "-E", "high", "-td", td.buildDir.toString, + "-o", "Foo")) + + Then("the output should be the same as using FIRRTL input") + new File(td.buildDir + "/Foo.hi.fir") should (exist) + } + + } + + info("As a FIRRTL command line user") + info("I want to receive error messages when I do not specify mandatory inputs") + feature("FirrtlMain input validation of mandatory options") { + scenario("User gives no command line options (no input circuit specified)") { + val f = new FirrtlMainFixture + + When("the user passes no arguments") + val (out, err, result) = grabStdOutErr { catchStatus { f.stage.main(Array.empty) } } + + Then("an error should be printed on stdout") + out should include (s"Error: Unable to determine FIRRTL source to read") + + And("no usage text should be shown") + out should not include ("Usage: firrtl") + + And("nothing should print to stderr") + err should be (empty) + + And("the exit code should be 1") + result should be (Left(1)) + } + } + + info("As a FIRRTL command line user") + info("I want to receive helpful error and warnings message") + feature("FirrtlMain input validation") { + /* Note: most input validation occurs inside firrtl.stage.phases.Checks. This seeks to validate command line + * behavior. + */ + + scenario("User tries to use an implicit annotation file") { + val f = new FirrtlMainFixture + val td = new TargetDirectoryFixture("implict-annotation-file") + val circuit = new SimpleFirrtlCircuitFixture + + And("implicit legacy and extant annotation files") + val annoFiles = Array( (new File(td.dir + "/Top.anno"), "/annotations/SampleAnnotations.anno"), + (new File(td.dir + "/Top.anno.json"), "/annotations/SampleAnnotations.anno.json") ) + annoFiles.foreach{ case (file, source) => copyResourceToFile(source, file) } + + When("the user implies an annotation file (an annotation file has the same base name as an input file)") + val in = new File(td.dir + "/Top.fir") + val pw = new PrintWriter(in) + pw.write(circuit.input) + pw.close() + val (out, _, result) = grabStdOutErr{ catchStatus { f.stage.main(Array("-td", td.dir.toString, + "-i", in.toString, + "-foaf", "Top.out", + "-X", "high", + "-E", "high")) } } + + Then("the implicit annotation file should NOT be read") + val annoFileOut = new File(td.dir + "/Top.out.anno.json") + val annotationJson = Source.fromFile(annoFileOut).mkString + annotationJson should not include ("InlineInstances") + + And("no warning should be printed") + out should not include ("Warning:") + + And("no error should be printed") + out should not include ("Error:") + + And("the exit code should be 0") + result shouldBe a [Right[_,_]] + } + + scenario("User provides unsupported legacy annotations") { + val f = new FirrtlMainFixture + val td = new TargetDirectoryFixture("legacy-annotation-file") + val circuit = new SimpleFirrtlCircuitFixture + + And("a legacy annotation file") + val annoFile = new File(td.dir + "/legacy.anno") + copyResourceToFile("/annotations/SampleAnnotations.anno", annoFile) + + When("the user provides legacy annotations") + val in = new File(td.dir + "/Top.fir") + val pw = new PrintWriter(in) + pw.write(circuit.input) + pw.close() + val (out, _, result) = grabStdOutErr{ catchStatus { f.stage.main(Array("-td", td.dir.toString, + "-i", in.toString, + "-faf", annoFile.toString, + "-foaf", "Top", + "-X", "high")) } } + + Then("a warning should be printed") + out should include ("YAML Annotation files are deprecated") + + And("the exit code should be 0") + result shouldBe a [Right[_,_]] + } + + Seq( + /* Erroneous inputs */ + FirrtlMainTest(args = Array("--thisIsNotASupportedOption"), + circuit = None, + stdout = Some("Error: Unknown option"), + result = 1), + FirrtlMainTest(args = Array("-i", "foo", "--info-mode", "Use"), + circuit = None, + stdout = Some("Unknown info mode 'Use'! (Did you misspell it?)"), + result = 1), + FirrtlMainTest(args = Array("-i", "test_run_dir/I-DO-NOT-EXIST"), + circuit = None, + stdout = Some("Input file 'test_run_dir/I-DO-NOT-EXIST' not found!"), + result = 1), + FirrtlMainTest(args = Array("-i", "foo", "-X", "Verilog"), + circuit = None, + stdout = Some("Unknown compiler name 'Verilog'! (Did you misspell it?)"), + result = 1) + ) + .foreach(runStageExpectFiles) + + } + + info("As a FIRRTL transform developer") + info("I want to register my custom transforms with FIRRTL") + feature("FirrtlMain transform registration") { + scenario("User doesn't know if their transforms were registered") { + val f = new FirrtlMainFixture + + When("the user passes '--show-registrations'") + val (out, _, result) = grabStdOutErr { catchStatus { f.stage.main(Array("--show-registrations")) } } + + Then("stdout should show registered transforms") + out should include ("firrtl.passes.InlineInstances") + + And("stdout should show registered libraries") + out should include("firrtl.passes.memlib.MemLibOptions") + + And("the exit code should be 1") + result should be (Left(1)) + } + } + + info("As a longtime FIRRTL user") + info("I migrate from Driver to FirrtlMain") + feature("FirrtlMain migration helpers") { + def optionRemoved(a: String): Option[String] = Some(s"Option '$a' was removed as part of the FIRRTL Stage refactor") + Seq( + /* Removed --top-name/-tn handling */ + FirrtlMainTest(args = Array("--top-name", "foo"), + circuit = None, + stdout = optionRemoved("--top-name/-tn"), + result = 1), + FirrtlMainTest(args = Array("-tn"), + circuit = None, + stdout = optionRemoved("--top-name/-tn"), + result = 1), + /* Removed --split-modules/-fsm handling */ + FirrtlMainTest(args = Array("--split-modules"), + circuit = None, + stdout = optionRemoved("--split-modules/-fsm"), + result = 1), + FirrtlMainTest(args = Array("-fsm"), + circuit = None, + stdout = optionRemoved("--split-modules/-fsm"), + result = 1) + ) + .foreach(runStageExpectFiles) + } +} diff --git a/src/test/scala/firrtlTests/stage/FirrtlOptionsViewSpec.scala b/src/test/scala/firrtlTests/stage/FirrtlOptionsViewSpec.scala new file mode 100644 index 00000000..cf8bfb26 --- /dev/null +++ b/src/test/scala/firrtlTests/stage/FirrtlOptionsViewSpec.scala @@ -0,0 +1,70 @@ +// See LICENSE for license details. + +package firrtlTests.stage + +import org.scalatest.{FlatSpec, Matchers} + +import firrtl.options._ +import firrtl.stage._ + +import firrtl.{CircuitForm, CircuitState, ir, NoneCompiler, Parser, UnknownForm} +import firrtl.options.Viewer.view +import firrtl.stage.{FirrtlOptions, FirrtlOptionsView} + +class BazCompiler extends NoneCompiler + +class Baz_Compiler extends NoneCompiler + +class FirrtlOptionsViewSpec extends FlatSpec with Matchers { + + behavior of FirrtlOptionsView.getClass.getName + + def circuitString(main: String): String = s"""|circuit $main: + | module $main: + | node x = UInt<1>("h0") + |""".stripMargin + + val corge: String = circuitString("corge") + + val grault: ir.Circuit = Parser.parse(circuitString("grault")) + + val annotations = Seq( + /* FirrtlOptions */ + OutputFileAnnotation("bar"), + CompilerAnnotation(new BazCompiler()), + InfoModeAnnotation("use"), + FirrtlCircuitAnnotation(grault) + ) + + it should "construct a view from an AnnotationSeq" in { + val out = view[FirrtlOptions](annotations) + + out.outputFileName should be (Some("bar")) + out.compiler.getClass should be (classOf[BazCompiler]) + out.infoModeName should be ("use") + out.firrtlCircuit should be (Some(grault)) + } + + /* This test only exists to catch changes to existing behavior. This test does not indicate that this is the correct + * behavior, only that modifications to existing code will not change behavior that people may expect. + */ + it should "overwrite or append to earlier annotation information with later annotation information" in { + val corge_ = circuitString("xyzzy_") + val grault_ = Parser.parse(circuitString("thud_")) + + val overwrites = Seq( + OutputFileAnnotation("bar_"), + CompilerAnnotation(new Baz_Compiler()), + InfoModeAnnotation("gen"), + FirrtlCircuitAnnotation(grault_) + ) + + val out = view[FirrtlOptions](annotations ++ overwrites) + + out.outputFileName should be (Some("bar_")) + out.compiler.getClass should be (classOf[Baz_Compiler]) + out.infoModeName should be ("gen") + out.firrtlCircuit should be (Some(grault_)) + } + +} diff --git a/src/test/scala/firrtlTests/stage/phases/AddCircuitSpec.scala b/src/test/scala/firrtlTests/stage/phases/AddCircuitSpec.scala new file mode 100644 index 00000000..a86c5f12 --- /dev/null +++ b/src/test/scala/firrtlTests/stage/phases/AddCircuitSpec.scala @@ -0,0 +1,101 @@ +// See LICENSE for license details. + +package firrtlTests.stage.phases + +import org.scalatest.{FlatSpec, Matchers} + +import firrtl.{AnnotationSeq, Parser} +import firrtl.annotations.NoTargetAnnotation +import firrtl.options.{OptionsException, Phase, PhasePrerequisiteException} +import firrtl.stage.{CircuitOption, FirrtlCircuitAnnotation, FirrtlSourceAnnotation, InfoModeAnnotation, + FirrtlFileAnnotation} +import firrtl.stage.phases.AddCircuit + +import java.io.{File, FileWriter} + +class AddCircuitSpec extends FlatSpec with Matchers { + + case class FooAnnotation(x: Int) extends NoTargetAnnotation + case class BarAnnotation(x: String) extends NoTargetAnnotation + + class Fixture { val phase: Phase = new AddCircuit } + + behavior of classOf[AddCircuit].toString + + def firrtlSource(name: String): String = + s"""|circuit $name: + | module $name: + | input a: UInt<1> + | output b: UInt<1> + | b <= not(a) + |""".stripMargin + + it should "throw a PhasePrerequisiteException if a CircuitOption exists without an InfoModeAnnotation" in + new Fixture { + {the [PhasePrerequisiteException] thrownBy phase.transform(Seq(FirrtlSourceAnnotation("foo")))} + .message should startWith ("An InfoModeAnnotation must be present") + } + + it should "do nothing if no CircuitOption annotations are present" in new Fixture { + val annotations = (1 to 10).map(FooAnnotation) ++ + ('a' to 'm').map(_.toString).map(BarAnnotation) :+ InfoModeAnnotation("ignore") + phase.transform(annotations).toSeq should be (annotations.toSeq) + } + + val (file, fileCircuit) = { + val source = firrtlSource("foo") + val fileName = "test_run_dir/AddCircuitSpec.fir" + val fw = new FileWriter(new File(fileName)) + fw.write(source) + fw.close() + (fileName, Parser.parse(source)) + } + + val (source, sourceCircuit) = { + val source = firrtlSource("bar") + (source, Parser.parse(source)) + } + + it should "transform and remove CircuitOption annotations" in new Fixture { + val circuit = Parser.parse(firrtlSource("baz")) + + val annotations = Seq( + FirrtlFileAnnotation(file), + FirrtlSourceAnnotation(source), + FirrtlCircuitAnnotation(circuit), + InfoModeAnnotation("ignore") ) + + val annotationsExpected = Set( + FirrtlCircuitAnnotation(fileCircuit), + FirrtlCircuitAnnotation(sourceCircuit), + FirrtlCircuitAnnotation(circuit) ) + + val out = phase.transform(annotations).toSeq + + info("generated expected FirrtlCircuitAnnotations") + out.collect{ case a: FirrtlCircuitAnnotation => a}.toSet should be (annotationsExpected) + + info("all CircuitOptions were removed") + out.collect{ case a: CircuitOption => a } should be (empty) + } + + it should """add info for a FirrtlFileAnnotation with a "gen" info mode""" in new Fixture { + phase.transform(Seq(InfoModeAnnotation("gen"), FirrtlFileAnnotation(file))) + .collectFirst{ case a: FirrtlCircuitAnnotation => a.circuit.serialize } + .get should include ("AddCircuitSpec") + } + + it should """add info for a FirrtlSourceAnnotation with an "append" info mode""" in new Fixture { + phase.transform(Seq(InfoModeAnnotation("append"), FirrtlSourceAnnotation(source))) + .collectFirst{ case a: FirrtlCircuitAnnotation => a.circuit.serialize } + .get should include ("anonymous source") + } + + it should "throw an OptionsException if the specified file doesn't exist" in new Fixture { + val a = Seq(InfoModeAnnotation("ignore"), FirrtlFileAnnotation("test_run_dir/I-DO-NOT-EXIST")) + + {the [OptionsException] thrownBy phase.transform(a)} + .message should startWith (s"Input file 'test_run_dir/I-DO-NOT-EXIST' not found") + } + +} diff --git a/src/test/scala/firrtlTests/stage/phases/AddDefaultsSpec.scala b/src/test/scala/firrtlTests/stage/phases/AddDefaultsSpec.scala new file mode 100644 index 00000000..c4566dd5 --- /dev/null +++ b/src/test/scala/firrtlTests/stage/phases/AddDefaultsSpec.scala @@ -0,0 +1,37 @@ +// See LICENSE for license details. + +package firrtlTests.stage.phases + +import org.scalatest.{FlatSpec, Matchers} + +import firrtl.NoneCompiler +import firrtl.annotations.Annotation +import firrtl.stage.phases.AddDefaults +import firrtl.transforms.BlackBoxTargetDirAnno +import firrtl.stage.{CompilerAnnotation, InfoModeAnnotation} +import firrtl.options.{Phase, TargetDirAnnotation} + +class AddDefaultsSpec extends FlatSpec with Matchers { + + class Fixture { val phase: Phase = new AddDefaults } + + behavior of classOf[AddDefaults].toString + + it should "add expected default annotations and nothing else" in new Fixture { + val expected = Seq( + (a: Annotation) => a match { case BlackBoxTargetDirAnno(b) => b == TargetDirAnnotation().directory }, + (a: Annotation) => a match { case CompilerAnnotation(b) => b.getClass == CompilerAnnotation().compiler.getClass }, + (a: Annotation) => a match { case InfoModeAnnotation(b) => b == InfoModeAnnotation().modeName } ) + + phase.transform(Seq.empty).zip(expected).map { case (x, f) => f(x) should be (true) } + } + + it should "not overwrite existing annotations" in new Fixture { + val input = Seq( + BlackBoxTargetDirAnno("foo"), + CompilerAnnotation(new NoneCompiler()), + InfoModeAnnotation("ignore")) + + phase.transform(input).toSeq should be (input) + } +} diff --git a/src/test/scala/firrtlTests/stage/phases/AddImplicitEmitterSpec.scala b/src/test/scala/firrtlTests/stage/phases/AddImplicitEmitterSpec.scala new file mode 100644 index 00000000..117e9c5f --- /dev/null +++ b/src/test/scala/firrtlTests/stage/phases/AddImplicitEmitterSpec.scala @@ -0,0 +1,45 @@ +// See LICENSE for license details. + +package firrtlTests.stage.phases + +import org.scalatest.{FlatSpec, Matchers} + +import firrtl.{EmitAllModulesAnnotation, EmitCircuitAnnotation, HighFirrtlEmitter, VerilogCompiler} +import firrtl.annotations.NoTargetAnnotation +import firrtl.options.Phase +import firrtl.stage.{CompilerAnnotation, RunFirrtlTransformAnnotation} +import firrtl.stage.phases.AddImplicitEmitter + +class AddImplicitEmitterSpec extends FlatSpec with Matchers { + + case class FooAnnotation(x: Int) extends NoTargetAnnotation + case class BarAnnotation(x: String) extends NoTargetAnnotation + + class Fixture { val phase: Phase = new AddImplicitEmitter } + + val someAnnos = Seq(FooAnnotation(1), FooAnnotation(2), BarAnnotation("bar")) + + behavior of classOf[AddImplicitEmitter].toString + + it should "do nothing if no CompilerAnnotation is present" in new Fixture { + phase.transform(someAnnos).toSeq should be (someAnnos) + } + + it should "add an EmitCircuitAnnotation derived from a CompilerAnnotation" in new Fixture { + val input = CompilerAnnotation(new VerilogCompiler) +: someAnnos + val expected = input.flatMap{ + case a@ CompilerAnnotation(b) => Seq(a, + RunFirrtlTransformAnnotation(b.emitter), + EmitCircuitAnnotation(b.emitter.getClass)) + case a => Some(a) + } + phase.transform(input).toSeq should be (expected) + } + + it should "not add an EmitCircuitAnnotation if an EmitAnnotation already exists" in new Fixture { + val input = Seq(CompilerAnnotation(new VerilogCompiler), + EmitAllModulesAnnotation(classOf[HighFirrtlEmitter])) ++ someAnnos + phase.transform(input).toSeq should be (input) + } + +} diff --git a/src/test/scala/firrtlTests/stage/phases/AddImplicitOutputFileSpec.scala b/src/test/scala/firrtlTests/stage/phases/AddImplicitOutputFileSpec.scala new file mode 100644 index 00000000..72d186b5 --- /dev/null +++ b/src/test/scala/firrtlTests/stage/phases/AddImplicitOutputFileSpec.scala @@ -0,0 +1,46 @@ +// See LICENSE for license details. + +package firrtlTests.stage.phases + +import org.scalatest.{FlatSpec, Matchers} + +import firrtl.{ChirrtlEmitter, EmitAllModulesAnnotation, Parser} +import firrtl.options.Phase +import firrtl.stage.{FirrtlCircuitAnnotation, OutputFileAnnotation} +import firrtl.stage.phases.AddImplicitOutputFile + +class AddImplicitOutputFileSpec extends FlatSpec with Matchers { + + class Fixture { val phase: Phase = new AddImplicitOutputFile } + + val foo = """|circuit Foo: + | module Foo: + | node a = UInt<1>("h0") + |""".stripMargin + + val circuit = Parser.parse(foo) + + behavior of classOf[AddImplicitOutputFile].toString + + it should "default to an output file named 'a'" in new Fixture { + phase.transform(Seq.empty).toSeq should be (Seq(OutputFileAnnotation("a"))) + } + + it should "set the output file based on a FirrtlCircuitAnnotation's main" in new Fixture { + val in = Seq(FirrtlCircuitAnnotation(circuit)) + val out = OutputFileAnnotation(circuit.main) +: in + phase.transform(in).toSeq should be (out) + } + + it should "do nothing if an OutputFileAnnotation or EmitAllModulesAnnotation already exists" in new Fixture { + + info("OutputFileAnnotation works") + val outputFile = Seq(OutputFileAnnotation("Bar"), FirrtlCircuitAnnotation(circuit)) + phase.transform(outputFile).toSeq should be (outputFile) + + info("EmitAllModulesAnnotation works") + val eam = Seq(EmitAllModulesAnnotation(classOf[ChirrtlEmitter]), FirrtlCircuitAnnotation(circuit)) + phase.transform(eam).toSeq should be (eam) + } + +} diff --git a/src/test/scala/firrtlTests/stage/phases/ChecksSpec.scala b/src/test/scala/firrtlTests/stage/phases/ChecksSpec.scala new file mode 100644 index 00000000..47aa9b5b --- /dev/null +++ b/src/test/scala/firrtlTests/stage/phases/ChecksSpec.scala @@ -0,0 +1,89 @@ +// See LICENSE for license details. + +package firrtlTests.stage.phases + +import org.scalatest.{FlatSpec, Matchers} + +import firrtl.stage._ + +import firrtl.{AnnotationSeq, ChirrtlEmitter, EmitAllModulesAnnotation, NoneCompiler} +import firrtl.options.{OptionsException, OutputAnnotationFileAnnotation, Phase} +import firrtl.stage.phases.{AddImplicitOutputFile, Checks} + +class ChecksSpec extends FlatSpec with Matchers { + + class Fixture { val phase: Phase = new Checks } + + val inputFile = FirrtlFileAnnotation("foo") + val outputFile = OutputFileAnnotation("bar") + val emitAllModules = EmitAllModulesAnnotation(classOf[ChirrtlEmitter]) + val outputAnnotationFile = OutputAnnotationFileAnnotation("baz") + val goodCompiler = CompilerAnnotation(new NoneCompiler()) + val infoMode = InfoModeAnnotation("ignore") + + val min = Seq(inputFile, goodCompiler, infoMode) + + def checkExceptionMessage(phase: Phase, annotations: AnnotationSeq, messageStart: String): Unit = + intercept[OptionsException]{ phase.transform(annotations) }.getMessage should startWith(messageStart) + + behavior of classOf[Checks].toString + + it should "require exactly one input source" in new Fixture { + info("0 input source causes an exception") + checkExceptionMessage(phase, Seq.empty, "Unable to determine FIRRTL source to read") + + info("2 input sources causes an exception") + val in = min :+ FirrtlSourceAnnotation("circuit Foo:") + checkExceptionMessage(phase, in, "Multiply defined input FIRRTL sources") + } + + it should "require mutual exclusivity of OutputFileAnnotation and EmitAllModulesAnnotation" in new Fixture { + info("OutputFileAnnotation alone works") + phase.transform(min :+ outputFile) + + info("OneFilePerModuleAnnotation alone works") + phase.transform(min :+ emitAllModules) + + info("Together, they throw an exception") + val in = min ++ Seq(outputFile, emitAllModules) + checkExceptionMessage(phase, in, "Output file is incompatible with emit all modules annotation") + } + + it should "enforce zero or one output files" in new Fixture { + val in = min ++ Seq(outputFile, outputFile) + checkExceptionMessage(phase, in, "No more than one output file can be specified") + } + + it should "enforce exactly one compiler" in new Fixture { + info("0 compilers should throw an exception") + val inZero = Seq(inputFile, infoMode) + checkExceptionMessage(phase, inZero, "Exactly one compiler must be specified, but none found") + + info("2 compilers should throw an exception") + val c = goodCompiler.compiler + val inTwo = min :+ goodCompiler + checkExceptionMessage(phase, inTwo, s"Exactly one compiler must be specified, but found '$c, $c'") + } + + it should "validate info mode names" in new Fixture { + info("Good info mode names should work") + Seq("ignore", "use", "gen", "append") + .map(info => phase.transform(Seq(inputFile, goodCompiler, InfoModeAnnotation(info)))) + } + + it should "enforce exactly one info mode" in new Fixture { + info("0 info modes should throw an exception") + checkExceptionMessage(phase, Seq(inputFile, goodCompiler), + "Exactly one info mode must be specified, but none found") + + info("2 info modes should throw an exception") + val i = infoMode.modeName + checkExceptionMessage(phase, min :+ infoMode, s"Exactly one info mode must be specified, but found '$i, $i'") + } + + it should "pass if the minimum annotations are specified" in new Fixture { + info(s"""Minimum required: ${min.map(_.getClass.getSimpleName).mkString(", ")}""") + phase.transform(min) + } + +} diff --git a/src/test/scala/firrtlTests/stage/phases/CompilerSpec.scala b/src/test/scala/firrtlTests/stage/phases/CompilerSpec.scala new file mode 100644 index 00000000..7a3ab6b7 --- /dev/null +++ b/src/test/scala/firrtlTests/stage/phases/CompilerSpec.scala @@ -0,0 +1,143 @@ +// See LICENSE for license details. + +package firrtlTests.stage.phases + +import org.scalatest.{FlatSpec, Matchers} + +import firrtl.{Compiler => _, _} +import firrtl.options.{Phase, PhasePrerequisiteException} +import firrtl.stage.{CompilerAnnotation, FirrtlCircuitAnnotation, RunFirrtlTransformAnnotation} +import firrtl.stage.phases.Compiler + +class CompilerSpec extends FlatSpec with Matchers { + + class Fixture { val phase: Phase = new Compiler } + + behavior of classOf[Compiler].toString + + it should "do nothing for an empty AnnotationSeq" in new Fixture { + phase.transform(Seq.empty).toSeq should be (empty) + } + + /** A circuit with a parameterized main (top name) that is different at High, Mid, and Low FIRRTL forms. */ + private def chirrtl(main: String): String = + s"""|circuit $main: + | module $main: + | output foo: {bar: UInt} + | + | foo.bar <= UInt<4>("h0") + |""".stripMargin + + it should "compile a circuit to Low FIRRTL when using the Verilog compiler" in new Fixture { + val compiler = new VerilogCompiler + + val circuitIn = Parser.parse(chirrtl("top")) + val circuitOut = compiler.compile(CircuitState(circuitIn, ChirrtlForm), Seq.empty).circuit + + val input = Seq( + FirrtlCircuitAnnotation(circuitIn), + CompilerAnnotation(compiler) ) + + val expected = Seq(FirrtlCircuitAnnotation(circuitOut)) + + phase.transform(input).toSeq should be (expected) + } + + it should "compile multiple FirrtlCircuitAnnotations" in new Fixture { + val (nc, hfc, mfc, lfc, vc, svc) = ( + new NoneCompiler, + new HighFirrtlCompiler, + new MiddleFirrtlCompiler, + new LowFirrtlCompiler, + new VerilogCompiler, + new SystemVerilogCompiler ) + val (ce, hfe, mfe, lfe, ve, sve) = ( + new ChirrtlEmitter, + new HighFirrtlEmitter, + new MiddleFirrtlEmitter, + new LowFirrtlEmitter, + new VerilogEmitter, + new SystemVerilogEmitter ) + + val a = Seq( + /* Default Compiler is HighFirrtlCompiler */ + CompilerAnnotation(hfc), + + /* First compiler group, use NoneCompiler */ + FirrtlCircuitAnnotation(Parser.parse(chirrtl("a"))), + CompilerAnnotation(nc), + RunFirrtlTransformAnnotation(ce), + EmitCircuitAnnotation(ce.getClass), + + /* Second compiler group, use default HighFirrtlCompiler */ + FirrtlCircuitAnnotation(Parser.parse(chirrtl("b"))), + RunFirrtlTransformAnnotation(ce), + EmitCircuitAnnotation(ce.getClass), + RunFirrtlTransformAnnotation(hfe), + EmitCircuitAnnotation(hfe.getClass), + + /* Third compiler group, use MiddleFirrtlCompiler */ + FirrtlCircuitAnnotation(Parser.parse(chirrtl("c"))), + CompilerAnnotation(mfc), + RunFirrtlTransformAnnotation(ce), + EmitCircuitAnnotation(ce.getClass), + RunFirrtlTransformAnnotation(hfe), + EmitCircuitAnnotation(hfe.getClass), + RunFirrtlTransformAnnotation(mfe), + EmitCircuitAnnotation(mfe.getClass), + + /* Fourth compiler group, use LowFirrtlCompiler*/ + FirrtlCircuitAnnotation(Parser.parse(chirrtl("d"))), + CompilerAnnotation(lfc), + RunFirrtlTransformAnnotation(ce), + EmitCircuitAnnotation(ce.getClass), + RunFirrtlTransformAnnotation(hfe), + EmitCircuitAnnotation(hfe.getClass), + RunFirrtlTransformAnnotation(mfe), + EmitCircuitAnnotation(mfe.getClass), + RunFirrtlTransformAnnotation(lfe), + EmitCircuitAnnotation(lfe.getClass), + + /* Fifth compiler group, use VerilogCompiler */ + FirrtlCircuitAnnotation(Parser.parse(chirrtl("e"))), + CompilerAnnotation(vc), + RunFirrtlTransformAnnotation(ce), + EmitCircuitAnnotation(ce.getClass), + RunFirrtlTransformAnnotation(hfe), + EmitCircuitAnnotation(hfe.getClass), + RunFirrtlTransformAnnotation(mfe), + EmitCircuitAnnotation(mfe.getClass), + RunFirrtlTransformAnnotation(lfe), + EmitCircuitAnnotation(lfe.getClass), + RunFirrtlTransformAnnotation(ve), + EmitCircuitAnnotation(ve.getClass), + + /* Sixth compiler group, use SystemVerilogCompiler */ + FirrtlCircuitAnnotation(Parser.parse(chirrtl("f"))), + CompilerAnnotation(svc), + RunFirrtlTransformAnnotation(ce), + EmitCircuitAnnotation(ce.getClass), + RunFirrtlTransformAnnotation(hfe), + EmitCircuitAnnotation(hfe.getClass), + RunFirrtlTransformAnnotation(mfe), + EmitCircuitAnnotation(mfe.getClass), + RunFirrtlTransformAnnotation(lfe), + EmitCircuitAnnotation(lfe.getClass), + RunFirrtlTransformAnnotation(sve), + EmitCircuitAnnotation(sve.getClass) + ) + + val output = phase.transform(a) + + info("with the same number of output FirrtlCircuitAnnotations") + output + .collect{ case a: FirrtlCircuitAnnotation => a } + .size should be (6) + + info("and all expected EmittedAnnotations should be generated") + output + .collect{ case a: EmittedAnnotation[_] => a } + .size should be (20) + } + +} diff --git a/src/test/scala/firrtlTests/stage/phases/WriteEmittedSpec.scala b/src/test/scala/firrtlTests/stage/phases/WriteEmittedSpec.scala new file mode 100644 index 00000000..4f7ad92a --- /dev/null +++ b/src/test/scala/firrtlTests/stage/phases/WriteEmittedSpec.scala @@ -0,0 +1,79 @@ +// See LICENSE for license details. + +package firrtlTests.stage.phases + +import org.scalatest.{FlatSpec, Matchers} + +import java.io.File + +import firrtl._ + +import firrtl.options.{Phase, TargetDirAnnotation} +import firrtl.stage.OutputFileAnnotation +import firrtl.stage.phases.WriteEmitted + +class WriteEmittedSpec extends FlatSpec with Matchers { + + def removeEmitted(a: AnnotationSeq): AnnotationSeq = a.flatMap { + case a: EmittedAnnotation[_] => None + case a => Some(a) + } + + class Fixture { val phase: Phase = new WriteEmitted } + + behavior of classOf[WriteEmitted].toString + + it should "write emitted circuits" in new Fixture { + val annotations = Seq( + TargetDirAnnotation("test_run_dir/WriteEmittedSpec"), + EmittedFirrtlCircuitAnnotation(EmittedFirrtlCircuit("foo", "", ".foocircuit")), + EmittedFirrtlCircuitAnnotation(EmittedFirrtlCircuit("bar", "", ".barcircuit")), + EmittedVerilogCircuitAnnotation(EmittedVerilogCircuit("baz", "", ".bazcircuit")) ) + val expected = Seq("foo.foocircuit", "bar.barcircuit", "baz.bazcircuit") + .map(a => new File(s"test_run_dir/WriteEmittedSpec/$a")) + + info("annotations are unmodified") + phase.transform(annotations).toSeq should be (removeEmitted(annotations).toSeq) + + expected.foreach{ a => + info(s"$a was written") + a should (exist) + a.delete() + } + } + + it should "default to the output file name if one exists" in new Fixture { + val annotations = Seq( + TargetDirAnnotation("test_run_dir/WriteEmittedSpec"), + OutputFileAnnotation("quux"), + EmittedFirrtlCircuitAnnotation(EmittedFirrtlCircuit("qux", "", ".quxcircuit")) ) + val expected = new File("test_run_dir/WriteEmittedSpec/quux.quxcircuit") + + info("annotations are unmodified") + phase.transform(annotations).toSeq should be (removeEmitted(annotations).toSeq) + + info(s"$expected was written") + expected should (exist) + expected.delete() + } + + it should "write emitted modules" in new Fixture { + val annotations = Seq( + TargetDirAnnotation("test_run_dir/WriteEmittedSpec"), + EmittedFirrtlModuleAnnotation(EmittedFirrtlModule("foo", "", ".foomodule")), + EmittedFirrtlModuleAnnotation(EmittedFirrtlModule("bar", "", ".barmodule")), + EmittedVerilogModuleAnnotation(EmittedVerilogModule("baz", "", ".bazmodule")) ) + val expected = Seq("foo.foomodule", "bar.barmodule", "baz.bazmodule") + .map(a => new File(s"test_run_dir/WriteEmittedSpec/$a")) + + info("EmittedComponent annotations are deleted") + phase.transform(annotations).toSeq should be (removeEmitted(annotations).toSeq) + + expected.foreach{ a => + info(s"$a was written") + a should (exist) + a.delete() + } + } + +} -- cgit v1.2.3