diff options
Diffstat (limited to 'src/test/scala/firrtlTests/options')
4 files changed, 280 insertions, 37 deletions
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 +} |
