diff --git a/build.sbt b/build.sbt index 89608e4b24..39999ae6a6 100644 --- a/build.sbt +++ b/build.sbt @@ -44,6 +44,7 @@ lazy val daffodil = project sapi, schematron, slf4jLogger, + tdmlJunit, tdmlLib, tdmlProc, testDaf, @@ -161,6 +162,11 @@ lazy val tdmlProc = Project("daffodil-tdml-processor", file("daffodil-tdml-proce .dependsOn(tdmlLib, codeGenC, core, slf4jLogger) .settings(commonSettings) +lazy val tdmlJunit = Project("daffodil-tdml-junit", file("daffodil-tdml-junit")) + .dependsOn(tdmlProc) + .settings(commonSettings) + .settings(libraryDependencies ++= Dependencies.junit) + lazy val cli = Project("daffodil-cli", file("daffodil-cli")) .dependsOn( tdmlProc, @@ -184,7 +190,7 @@ lazy val schematron = Project("daffodil-schematron", file("daffodil-schematron") .settings(libraryDependencies ++= Dependencies.schematron) lazy val testDaf = Project("daffodil-test", file("daffodil-test")) - .dependsOn(tdmlProc % "test", codeGenC % "test->test", udf % "test->test") + .dependsOn(tdmlJunit % "test", codeGenC % "test->test", udf % "test->test") .settings(commonSettings, nopublish) // // Uncomment the following line to run these tests @@ -193,7 +199,7 @@ lazy val testDaf = Project("daffodil-test", file("daffodil-test")) //.settings(IBMDFDLCrossTesterPlugin.settings) lazy val testIBM1 = Project("daffodil-test-ibm1", file("daffodil-test-ibm1")) - .dependsOn(tdmlProc % "test") + .dependsOn(tdmlJunit % "test") .settings(commonSettings, nopublish) // // Uncomment the following line to run these tests @@ -216,11 +222,11 @@ lazy val testIntegration = ) lazy val tutorials = Project("daffodil-tutorials", file("tutorials")) - .dependsOn(tdmlProc % "test") + .dependsOn(tdmlJunit % "test") .settings(commonSettings, nopublish) lazy val testStdLayout = Project("daffodil-test-stdLayout", file("test-stdLayout")) - .dependsOn(tdmlProc % "test") + .dependsOn(tdmlJunit % "test") .settings(commonSettings, nopublish) // Choices here are Java LTS versions, 8, 11, 17, 21,... diff --git a/daffodil-tdml-junit/src/main/scala/org/apache/daffodil/junit/tdml/TdmlSuite.scala b/daffodil-tdml-junit/src/main/scala/org/apache/daffodil/junit/tdml/TdmlSuite.scala new file mode 100644 index 0000000000..2ebb8d9dd9 --- /dev/null +++ b/daffodil-tdml-junit/src/main/scala/org/apache/daffodil/junit/tdml/TdmlSuite.scala @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.daffodil.junit.tdml + +import java.io.File + +import org.apache.daffodil.tdml.Runner +import org.apache.daffodil.tdml.TDMLTestNotCompatibleException + +import org.junit.AfterClass +import org.junit.AssumptionViolatedException +import org.junit.Rule +import org.junit.rules.TestName + +/** + * Mixin for a JUnit test suite companion object + */ +trait TdmlSuite { + + /** + * Resource path to the TDML file to use for this JUnit suite + */ + val tdmlResource: String + + /** + * Function to get the directory containing tdmlResource. Useful if createRunner is overridden + * to use the Runner(dir, file, ...) factory method + */ + def tdmlDir: String = new File(tdmlResource).getParent() + + /** + * Function to get basename of tdmlResource. Useful if createRunner is overridden to use the + * Runner(dir, file, ...) factory method + */ + def tdmlFile: String = new File(tdmlResource).getName() + + /** + * Default implementation to create a Runner using the tdmlResource. This function can be + * overridden if additional options need to be passed to the Runner factory object, for + * example to disable TDML validation. In most cases this should not be needed, since most + * parameters should not be changed by normal schema tests, or can be defined in the TDML + * file. + */ + def createRunner(): Runner = Runner(tdmlResource) + + /** + * Lazily build the runner when needed + */ + final lazy val runner = createRunner() + + /** + * Ensure all resources associated with the Runner (e.g. cached compiled schemas, parsed TDML + * XML) to be freed once the test suite has completed + */ + @AfterClass + final def shutDown(): Unit = runner.reset +} + +/** + * Mixin for a JUnit test suite companion class + */ +trait TdmlTests { + + /** + * The companion object that contains the runner to be used for tests in this class + */ + val tdmlSuite: TdmlSuite + + /** + * Run a test from the runner with the same name as the JUnit test + * + * If a tdml test is not compatible with the DFDL implementation used for the test, we mark + * test test as having violated an assumption. This prevents the test from generating a test + * case failure, and instead it is marked as "skipped". + */ + final def test: Unit = { + try { + tdmlSuite.runner.runOneTest(testName) + } catch { + case e: TDMLTestNotCompatibleException => + throw new AssumptionViolatedException(e.getMessage, e) + } + } + + /** + * Helper function to easily trace a test. This should only be used for debugging and running + * a single test since "trace" modifies the state of the Runner causing all later tests in the + * suite to run with trace enabled + */ + final def trace: Unit = { + tdmlSuite.runner.trace + test + } + + /** + * Helper function to get the current JUnit test name. Useful for some tests that might want + * to provide special diagnostics about the test name + */ + final def testName = _name.getMethodName + + /** + * JUnit magic for setting the test name. Because the way scala turns vals into class members + * + methods, the Rule annotation must be on a def + */ + private val _name: TestName = new TestName() + @Rule final def name = _name +} diff --git a/daffodil-tdml-junit/src/main/scala/org/apache/daffodil/junit/tdml/package.scala b/daffodil-tdml-junit/src/main/scala/org/apache/daffodil/junit/tdml/package.scala new file mode 100644 index 0000000000..b64f0285fd --- /dev/null +++ b/daffodil-tdml-junit/src/main/scala/org/apache/daffodil/junit/tdml/package.scala @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.daffodil.junit + +/** + *

Overview

+ * + * Provides two traits to reduce boilerplate related to defining TDML tests: + * + * [[TdmlSuite]]: mixed into a Junit test suite companion object ensures the TDML Runner is + * created and cleaned up appropriately. Direct access to the runner is available, though most + * tests should use the "test" or "trace" methods defined in [[TdmlTests]]. Note that a + * TdmlSuite only supports a single TDML file and runner. If multiple are needed, additional + * test class objects must be created. The TdmlSuite also only supports creating a Runner with + * the default arguments. This is sufficient in most cases, but if a different runner is needed, + * implementations can override the [[TdmlSuite.createRunner]] function. + * + * [[TdmlTests]]: contains [[TdmlTests.test]] and [[TdmlTests.trace]] helper functions to easily + * run or trace tests without having to provide the runner or the test name. The JUnit test name + * is used as the parameter passed to runOneTest (i.e. the name of the test in the TDML file). + * The runner to use is the runner associated with the companion object, which must be set in + * the [[TdmlTests.tdmlSuite]] val + * + *

Examples

+ * + * The following example defines a JUnit test suite called "MyTests". The companion object mixes + * in TdmlSuite and defines the tdmlResoruce to use for all tests in the suite. The companion + * class mixes in TdmlTests and provides a reference to the companion object via the tdmlSuite + * val, and defines a number of JUnit tests that each call the "test" method to run the TDML + * test with the same name as the JUnit test. + * {{{ + * import org.apache.daffodil.junit.tdml.TdmlSuite + * import org.apache.daffodil.junit.tdml.TdmlTests + * + * import org.junit.Tests + * + * object MyTests extends TdmlSuite { + * val tdmlResource = "/resource/path/to/tests.tdml" + * } + * + * class MyTests extends TdmlTests { + * val tdmlSuite = MyTests + * + * @Test def test1 = test + * @Test def test2 = test + * @Test def test3 = test + * } + *}}} + */ +package object tdml diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 982582353d..399931df89 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -50,6 +50,10 @@ object Dependencies { "net.sf.expectit" % "expectit-core" % "0.9.0" % "test" ) + lazy val junit = Seq( + "junit" % "junit" % "4.13.2" + ) + lazy val test = Seq( "junit" % "junit" % "4.13.2" % "test", "com.github.sbt" % "junit-interface" % "0.13.3" % "test",