├── settings.gradle.kts ├── images └── dynatest.png ├── .gitignore ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── dynatest-engine ├── src │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── services │ │ │ │ └── org.junit.platform.engine.TestEngine │ │ └── kotlin │ │ │ └── com │ │ │ └── github │ │ │ └── mvysny │ │ │ └── dynatest │ │ │ ├── InternalTestingClass.kt │ │ │ ├── DynaTest.kt │ │ │ └── engine │ │ │ ├── Utils.kt │ │ │ ├── DynaNodeImpl.kt │ │ │ ├── TestSourceUtils.kt │ │ │ └── DynaTestEngine.kt │ └── test │ │ └── kotlin │ │ └── com │ │ └── github │ │ └── mvysny │ │ └── dynatest │ │ ├── _UniqueIdCheckupClass.kt │ │ ├── UtilsTest.kt │ │ ├── SanityTest.kt │ │ ├── CalculatorTest.kt │ │ ├── TestSourceUtilsTest.kt │ │ ├── GeneralDynaTestEngineTest.kt │ │ ├── DynaRunner.kt │ │ ├── TestUtilsTest.kt │ │ └── DynaTestEngineTest.kt └── build.gradle.kts ├── dynatest ├── build.gradle.kts └── src │ ├── test │ └── kotlin │ │ └── com │ │ └── github │ │ └── mvysny │ │ └── dynatest │ │ ├── LateinitPropertyTest.kt │ │ ├── TestUtilsTest.kt │ │ └── FileTestUtilsTest.kt │ └── main │ └── kotlin │ └── com │ └── github │ └── mvysny │ └── dynatest │ ├── LateinitProperty.kt │ ├── TestUtils.kt │ └── FileTestUtils.kt ├── dynatest-api ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── github │ │ └── mvysny │ │ └── dynatest │ │ ├── Outcome.kt │ │ ├── DynatestApiUtils.kt │ │ └── DynaNode.kt │ └── test │ └── kotlin │ └── com │ └── github │ └── mvysny │ └── dynatest │ ├── ApiTestClass.kt │ └── UncompilableTest.kt ├── .github └── workflows │ └── gradle.yml ├── CONTRIBUTING.md ├── gradlew.bat ├── gradlew ├── LICENSE.txt └── README.md /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include("dynatest-api", "dynatest-engine", "dynatest") 2 | -------------------------------------------------------------------------------- /images/dynatest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvysny/dynatest/HEAD/images/dynatest.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | build 3 | .gradle 4 | *.iml 5 | out 6 | local.properties 7 | .attach_pid* 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvysny/dynatest/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /dynatest-engine/src/main/resources/META-INF/services/org.junit.platform.engine.TestEngine: -------------------------------------------------------------------------------- 1 | com.github.mvysny.dynatest.engine.DynaTestEngine -------------------------------------------------------------------------------- /dynatest/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api(project(":dynatest-engine")) 3 | } 4 | 5 | kotlin { 6 | explicitApi() 7 | } 8 | 9 | val publishing = ext["publishing"] as (artifactId: String) -> Unit 10 | publishing("dynatest") 11 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /dynatest-api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api(kotlin("stdlib-jdk8")) 3 | api(kotlin("test")) 4 | testRuntimeOnly(libs.junit.jupiter.engine) 5 | } 6 | 7 | kotlin { 8 | explicitApi() 9 | } 10 | 11 | val publishing = ext["publishing"] as (artifactId: String) -> Unit 12 | publishing("dynatest-api") 13 | -------------------------------------------------------------------------------- /dynatest-engine/src/test/kotlin/com/github/mvysny/dynatest/_UniqueIdCheckupClass.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | 3 | class _UniqueIdCheckupClass : DynaTest({ 4 | test("root test") {} 5 | group("root group") { 6 | test("nested") {} 7 | group("nested group") { 8 | test("nested nested") {} 9 | } 10 | test("nested2") {} 11 | } 12 | test("root test 2") {} 13 | }) -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | junit-jupiter = "5.10.2" # https://repo1.maven.org/maven2/org/junit/jupiter/junit-jupiter-engine/ 3 | junit-platform = "1.10.2" # https://repo1.maven.org/maven2/org/junit/platform/junit-platform-engine/ 4 | 5 | [libraries] 6 | junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit-jupiter" } 7 | junit-platform-engine = { module = "org.junit.platform:junit-platform-engine", version.ref = "junit-platform" } 8 | junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit-jupiter" } 9 | -------------------------------------------------------------------------------- /dynatest/src/test/kotlin/com/github/mvysny/dynatest/LateinitPropertyTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | 3 | import java.io.File 4 | import kotlin.test.expect 5 | 6 | class LateinitPropertyTest : DynaTest({ 7 | test("testFailsIfNoValue") { 8 | val file: File by late() 9 | expectThrows(RuntimeException::class, "LateinitProperty(name=file, value=null): not initialized") { 10 | file.name 11 | } 12 | } 13 | 14 | test("simple") { 15 | var file: File by late() 16 | file = File("foo.txt") 17 | expect("foo.txt") { file.name } 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /dynatest-engine/src/test/kotlin/com/github/mvysny/dynatest/UtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | 3 | import com.github.mvysny.dynatest.engine.toFile 4 | import java.io.File 5 | import java.net.URI 6 | import java.net.URL 7 | import kotlin.test.expect 8 | 9 | class UtilsTest : DynaTest({ 10 | group("toFile") { 11 | test("file:///tmp") { 12 | expect(File("/tmp")) { URL("file:///tmp").toFile() } 13 | expect(File("/tmp")) { URI("file:///tmp").toFile() } 14 | } 15 | test("http:///foo.fi") { 16 | expect(null) { URL("http:///foo.fi").toFile() } 17 | expect(null) { URI("http:///foo.fi").toFile() } 18 | } 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Gradle 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, macos-latest, windows-latest] 11 | java: [11, 17, 21] 12 | 13 | runs-on: ${{ matrix.os }} 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up JDK ${{ matrix.java }} 18 | uses: actions/setup-java@v3 19 | with: 20 | java-version: ${{ matrix.java }} 21 | distribution: 'temurin' 22 | - name: Cache Gradle packages 23 | uses: actions/cache@v2 24 | with: 25 | path: | 26 | ~/.gradle/caches 27 | ~/.gradle/wrapper 28 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts', 'gradle/wrapper/gradle-wrapper.properties', 'gradle.properties') }} 29 | - name: Build with Gradle 30 | run: ./gradlew --stacktrace --info --no-daemon 31 | 32 | -------------------------------------------------------------------------------- /dynatest-api/src/main/kotlin/com/github/mvysny/dynatest/Outcome.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | 3 | /** 4 | * The outcome of a test run. Provided as a parameter to the block specified 5 | * in the [DynaNodeGroup.afterEach] and [DynaNodeGroup.afterGroup] functions. 6 | * @property testName the test name; `null` in the `afterGroup` block. 7 | * @property failureCause if not null then either the test, or one of [DynaNodeGroup.afterEach] and [DynaNodeGroup.afterGroup] 8 | * have failed with an exception. 9 | */ 10 | public data class Outcome(val testName: String?, val failureCause: Throwable?) { 11 | /** 12 | * If true then the test and all previously called [DynaNodeGroup.afterEach] and [DynaNodeGroup.afterGroup] have succeeded. 13 | */ 14 | val isSuccess: Boolean get() = failureCause == null 15 | /** 16 | * If true then [isSuccess] is false. 17 | */ 18 | val isFailure: Boolean get() = !isSuccess 19 | } 20 | -------------------------------------------------------------------------------- /dynatest-engine/src/main/kotlin/com/github/mvysny/dynatest/InternalTestingClass.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | 3 | import com.github.mvysny.dynatest.engine.DynaNodeGroupImpl 4 | import com.github.mvysny.dynatest.engine.toTestSource 5 | import org.junit.platform.engine.support.descriptor.ClassSource 6 | 7 | /** 8 | * This whole file is only for the purposes of testing of resolving class name to TestSource. Please ignore this file. 9 | */ 10 | 11 | 12 | internal fun internalTestingClassGetTestSourceOfThis(): StackTraceElement = DynaNodeGroupImpl.computeTestSource()!! 13 | 14 | internal class InternalTestingClass { 15 | companion object { 16 | @JvmStatic 17 | internal fun getTestSourceOfThis(): ClassSource = DynaNodeGroupImpl.computeTestSource()!!.toTestSource() as ClassSource 18 | 19 | /** 20 | * A nasty test. This test will make Gradle freeze after last test. 21 | * A Test for https://github.com/gradle/gradle/issues/5737 22 | */ 23 | @JvmStatic 24 | internal fun gradleFreezingTest(g: DynaNodeGroup) { 25 | g.test("because ") {} 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Developing 2 | 3 | Please feel free to open bug reports to discuss new features; PRs are welcome as well :) 4 | 5 | All utility methods should go into the [dynatest](dynatest) project. Only contribute 6 | to [dynatest-api](dynatest-api) and [dynatest-engine](dynatest-engine) if there is 7 | something wrong with the engine itself. The goal is to keep the `-api` and `-engine` 8 | projects as small as possible, placing all utility methods into the `dynatest` project. 9 | 10 | # Releasing 11 | 12 | To release the library to Maven Central: 13 | 14 | 1. Edit `build.gradle.kts` and remove `-SNAPSHOT` in the `version=` stanza 15 | 2. Commit with the commit message of simply being the version being released, e.g. "0.20" 16 | 3. git tag the commit with the same tag name as the commit message above, e.g. `0.20` 17 | 4. `git push`, `git push --tags` 18 | 5. Run `./gradlew clean build publish` 19 | 6. Continue to the [OSSRH Nexus](https://oss.sonatype.org/#stagingRepositories) and follow the [release procedure](https://central.sonatype.org/pages/releasing-the-deployment.html). 20 | 7. Add the `-SNAPSHOT` back to the `version=` while increasing the version to something which will be released in the future, 21 | e.g. 0.21-SNAPSHOT, then commit with the commit message "0.21-SNAPSHOT" and push. 22 | -------------------------------------------------------------------------------- /dynatest-engine/src/main/kotlin/com/github/mvysny/dynatest/DynaTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | 3 | import com.github.mvysny.dynatest.engine.DynaNodeGroupImpl 4 | import org.junit.platform.commons.annotation.Testable 5 | 6 | /** 7 | * Inherit from this class to write the tests: 8 | * ``` 9 | * class PhotoListTest : DynaTest({ 10 | * lateinit var photoList: PhotoList 11 | * beforeGroup { photoList = PhotoList() } 12 | * 13 | * group("tests of the `list()` method") { 14 | * test("initially the list must be empty") { 15 | * expect(true) { photoList.list().isEmpty } 16 | * } 17 | * } 18 | * ... 19 | * }) 20 | * ``` 21 | * @param block add groups and tests within this block, to register them to a test suite. 22 | */ 23 | @Testable 24 | public abstract class DynaTest(block: DynaNodeGroup.()->Unit) { 25 | /** 26 | * The "root" group which will nest all groups and tests produced by the initialization block. 27 | */ 28 | internal val root = DynaNodeGroupImpl( 29 | javaClass.simpleName, 30 | StackTraceElement(javaClass.name, "", null, -1), 31 | true 32 | ) 33 | init { 34 | root.block() 35 | } 36 | 37 | @Testable 38 | public fun blank() { 39 | // must be here, otherwise Intellij won't launch this class as a test (via rightclick). 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /dynatest/src/main/kotlin/com/github/mvysny/dynatest/LateinitProperty.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | 3 | import java.lang.RuntimeException 4 | import kotlin.properties.ReadWriteProperty 5 | import kotlin.reflect.KProperty 6 | 7 | /** 8 | * A very simple implementation of [ReadWriteProperty] which implements the semantics of `lateinit`. 9 | * 10 | * Allows you to create a reusable `withXYZ()` function. See README.md for more details. 11 | * 12 | * See [withTempDir] and the DynaTest documentation on how to use this class in your projects. 13 | */ 14 | public data class LateinitProperty(val name: String, private var value: V? = null) : ReadWriteProperty { 15 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: V) { 16 | this.value = value 17 | } 18 | 19 | override fun getValue(thisRef: Any?, property: KProperty<*>): V { 20 | return value ?: throw RuntimeException("$this: not initialized") 21 | } 22 | } 23 | 24 | /** 25 | * Internal, used by [late]. 26 | */ 27 | public class LateinitPropertyProvider { 28 | public operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): ReadWriteProperty = 29 | LateinitProperty(prop.name) 30 | } 31 | 32 | /** 33 | * Allows you to write lateinit variables as follows: 34 | * ``` 35 | * var file: File by late() 36 | * beforeEach { file = File.createTempFile("foo", "bar") } 37 | * afterEach { file.delete() } 38 | * test("something") { 39 | * file.expectExists() 40 | * } 41 | * ``` 42 | * 43 | * However, to create a reusable `withXYZ()` function, see [LateinitProperty] directly. 44 | */ 45 | public fun late(): LateinitPropertyProvider = LateinitPropertyProvider() 46 | -------------------------------------------------------------------------------- /dynatest-engine/src/main/kotlin/com/github/mvysny/dynatest/engine/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest.engine 2 | 3 | import java.io.File 4 | import java.net.URI 5 | import java.net.URL 6 | import java.net.URLClassLoader 7 | import java.nio.file.FileSystemNotFoundException 8 | import java.nio.file.Paths 9 | 10 | internal var pretendIsRunningInsideGradle: Boolean? = null 11 | 12 | internal val isRunningInsideGradle: Boolean get() { 13 | // testing purposes 14 | if (pretendIsRunningInsideGradle != null) return pretendIsRunningInsideGradle!! 15 | 16 | // if this function fails, Gradle will freeze. What the fuck! 17 | try { 18 | val classLoader: ClassLoader = Thread.currentThread().contextClassLoader 19 | if (classLoader is URLClassLoader) { 20 | // JDK 8 and older 21 | val jars: List = classLoader.urLs.toList() 22 | return (jars.any { it.toString().contains("gradle-worker.jar") }) 23 | } 24 | // JDK 9+ uses AppClassLoader which doesn't provide a list of URLs for us. 25 | // we need to check in a different way whether there is `gradle-worker.jar` on the classpath. 26 | // we know that it contains the worker/org/gradle/api/JavaVersion.class 27 | val workerJar: URL? = classLoader.getResource("worker/org/gradle/api/JavaVersion.class") 28 | return workerJar.toString().contains("gradle-worker.jar") 29 | } catch (t: Throwable) { 30 | // give up, just pretend that we're inside of Gradle 31 | t.printStackTrace() // to see these stacktraces run Gradle with --info 32 | return true 33 | } 34 | } 35 | 36 | internal fun URI.toFile(): File? = try { 37 | Paths.get(this).toFile() 38 | } catch (e: FileSystemNotFoundException) { 39 | null 40 | } 41 | internal fun URL.toFile(): File? = toURI().toFile() 42 | -------------------------------------------------------------------------------- /dynatest-engine/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api(project(":dynatest-api")) 3 | api(libs.junit.jupiter.api) 4 | api(libs.junit.platform.engine) 5 | testRuntimeOnly(libs.junit.jupiter.engine) 6 | } 7 | 8 | kotlin { 9 | explicitApi() 10 | } 11 | 12 | val publishing = ext["publishing"] as (artifactId: String) -> Unit 13 | publishing("dynatest-engine") 14 | 15 | /** 16 | * Counts all occurrences of [substring] within the receiver string. 17 | */ 18 | fun String.countOccurrences(substring: String) = 19 | indices.count { substring(it).startsWith(substring) } 20 | 21 | // verify that Gradle ran tests for all test classes and didn't ignore DynaTests 22 | tasks.named("test") { doLast { 23 | val testClasses: Array = file("src/test/kotlin/com/github/mvysny/dynatest").list()!! 24 | val expectedTests: List = testClasses 25 | .filter { it.endsWith("Test.kt") } 26 | .map { "TEST-com.github.mvysny.dynatest.${it.removeSuffix(".kt")}.xml" } 27 | .sorted() 28 | val actualTests: List = file("build/test-results/test") 29 | .list()!! 30 | .filter { it.endsWith(".xml") && !it.contains("_UniqueIdCheckupClass") } 31 | .sorted() 32 | if (expectedTests != actualTests) { 33 | throw RuntimeException("build.gradle.kts: Expected tests to run: $expectedTests got $actualTests") 34 | } 35 | 36 | // verify that Gradle runs all tests even if they are same-named (but different UniqueId) 37 | val testXmlPath = "build/test-results/test/TEST-com.github.mvysny.dynatest.DynaTestEngineTest.xml" 38 | val xml = file(testXmlPath).readText() 39 | val testcases = xml.countOccurrences("Unit) 8 | 9 | // this class lists all allowed cases of usages of the API and must always compile 10 | class ApiTestClass : DynaTest({ 11 | beforeEach { } 12 | beforeGroup { } 13 | afterEach { outcome: Outcome -> 14 | } 15 | afterEach { } 16 | afterGroup { outcome: Outcome -> 17 | } 18 | afterGroup { } 19 | 20 | group("group") { 21 | beforeEach { } 22 | beforeGroup { } 23 | afterEach { } 24 | afterGroup { } 25 | group("nested group") { 26 | test("a test") {} 27 | xtest("commented out test") {} 28 | } 29 | beforeEach { } 30 | beforeGroup { } 31 | afterEach { } 32 | afterGroup { } 33 | xgroup("commented out group") { 34 | group("nested group") { 35 | test("test") {} 36 | xtest("test") {} 37 | } 38 | test("test") {} 39 | xtest("test") {} 40 | } 41 | beforeEach { } 42 | beforeGroup { } 43 | afterEach { } 44 | afterGroup { } 45 | } 46 | xgroup("commented out group") { 47 | lateinit var something: String 48 | beforeEach { something = "foo" } // we should be able to have a lateinit variable and modify it 49 | 50 | group("nested group") { 51 | test("a test") { println(something) } 52 | xtest("commented out test") {} 53 | } 54 | xgroup("commented out group") { 55 | group("nested group") { 56 | test("test") {} 57 | xtest("test") {} 58 | } 59 | test("test") {} 60 | xtest("test") {} 61 | } 62 | } 63 | }) 64 | -------------------------------------------------------------------------------- /dynatest-engine/src/test/kotlin/com/github/mvysny/dynatest/SanityTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | 3 | import com.github.mvysny.dynatest.engine.DynaNodeGroupImpl 4 | import org.junit.jupiter.api.Assertions 5 | import org.junit.jupiter.api.Test 6 | import kotlin.test.expect 7 | 8 | /** 9 | * If one of these tests fail, there is a really glaring bug in the DynaTest framework. Other test suites will then produce completely 10 | * incorrect results and should be ignored. 11 | */ 12 | class SanityTest { 13 | @Test 14 | fun testComputeTestSource() { 15 | val e = DynaNodeGroupImpl.computeTestSource()!! 16 | expect(SanityTest::class.java.name) { e.className } 17 | expect("testComputeTestSource") { e.methodName } 18 | } 19 | 20 | @Test 21 | fun simple() { 22 | var ran = false 23 | runTests { 24 | test("simple") { 25 | ran = true 26 | } 27 | } 28 | expect(true) { ran } 29 | } 30 | 31 | @Test 32 | fun simpleInGroups() { 33 | var ran = false 34 | runTests { 35 | group("group1") { 36 | group("group2") { 37 | test("simple") { 38 | ran = true 39 | } 40 | } 41 | } 42 | } 43 | expect(true) { ran } 44 | } 45 | 46 | @Test 47 | fun simpleAfter() { 48 | var ran = false 49 | runTests { 50 | group("group1") { 51 | group("group2") { 52 | test("simple") {} 53 | afterEach { ran = true } 54 | } 55 | } 56 | } 57 | expect(true) { ran } 58 | } 59 | 60 | @Test 61 | fun testProperlyRethrowsException() { 62 | Assertions.assertThrows(TestFailedException::class.java) { 63 | runTests { 64 | test("always fail") { 65 | throw RuntimeException("Simulated failure") 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /dynatest-engine/src/test/kotlin/com/github/mvysny/dynatest/CalculatorTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | import kotlin.test.expect 3 | 4 | class Calculator { 5 | fun plusOne(i: Int) = i + 1 6 | fun close() {} 7 | } 8 | 9 | /** 10 | * A test case. 11 | */ 12 | class CalculatorTest : DynaTest({ 13 | 14 | /** 15 | * Top-level test. 16 | */ 17 | test("calculator instantiation test") { 18 | Calculator() 19 | } 20 | 21 | // you can have as many groups as you like, and you can nest them 22 | group("tests the plusOne() function") { 23 | 24 | // demo of the very simple test 25 | test("one plusOne") { 26 | expect(2) { Calculator().plusOne(1) } 27 | } 28 | 29 | // nested group 30 | group("positive numbers") { 31 | // you can even create a reusable test battery, call it from anywhere and use any parameters you like. 32 | calculatorBattery(0..10) 33 | calculatorBattery(100..110) 34 | } 35 | 36 | group("negative numbers") { 37 | calculatorBattery(-50..-40) 38 | } 39 | } 40 | }) 41 | 42 | /** 43 | * Demonstrates a reusable test battery which can be called repeatedly and parametrized. 44 | * @receiver all tests+groups do not run immediately, but instead they register themselves to this group; they are run later on 45 | * when launched by JUnit5 46 | * @param range parametrized battery demo 47 | */ 48 | @DynaTestDsl 49 | fun DynaNodeGroup.calculatorBattery(range: IntRange) { 50 | require(!range.isEmpty()) 51 | 52 | group("plusOne on $range") { 53 | lateinit var c: Calculator 54 | 55 | // analogous to @Before in JUnit4, or @BeforeEach in JUnit5 56 | beforeEach { c = Calculator() } 57 | // analogous to @After in JUnit4, or @AfterEach in JUnit5 58 | afterEach { c.close() } 59 | 60 | // we can even generate test cases in a loop 61 | for (i in range) { 62 | test("plusOne($i) == ${i + 1}") { 63 | expect(i + 1) { c.plusOne(i) } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /dynatest-api/src/main/kotlin/com/github/mvysny/dynatest/DynatestApiUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | 3 | import java.io.PrintWriter 4 | import java.io.StringWriter 5 | import kotlin.reflect.KClass 6 | import kotlin.test.assertFailsWith 7 | 8 | /** 9 | * Expects that given block fails with an exception of type [clazz] (or its subtype). 10 | * 11 | * Note that this is different from [assertFailsWith] since this function 12 | * also asserts on [Throwable.message]. 13 | * @param expectMessage optional substring which the [Throwable.message] must contain. 14 | * @throws AssertionError if the block completed successfully or threw some other exception. 15 | * @return the exception thrown, so that you can assert on it. 16 | */ 17 | public fun expectThrows(clazz: KClass, expectMessage: String = "", block: ()->Unit): T { 18 | // tests for this function are present in the dynatest-engine project 19 | val ex = assertFailsWith(clazz, block) 20 | if (!(ex.message ?: "").contains(expectMessage)) { 21 | throw AssertionError("${clazz.javaObjectType.name} message: Expected '$expectMessage' but was '${ex.message}'", ex) 22 | } 23 | return ex 24 | } 25 | 26 | /** 27 | * Expects that given block fails with an exception of type [T] (or its subtype). 28 | * 29 | * Note that this is different from [assertFailsWith] since this function 30 | * also asserts on [Throwable.message]. 31 | * @param expectMessage optional substring which the [Throwable.message] must contain. 32 | * @throws AssertionError if the block completed successfully or threw some other exception. 33 | * @return the exception thrown, so that you can assert on it. 34 | */ 35 | public inline fun expectThrows(expectMessage: String = "", noinline block: ()->Unit): T = 36 | expectThrows(T::class, expectMessage, block) 37 | 38 | /** 39 | * Handy function to get a stack trace from receiver. 40 | */ 41 | public fun Throwable.getStackTraceAsString(): String { 42 | val sw = StringWriter() 43 | val pw = PrintWriter(sw) 44 | printStackTrace(pw) 45 | pw.flush() 46 | return sw.toString() 47 | } 48 | -------------------------------------------------------------------------------- /dynatest/src/main/kotlin/com/github/mvysny/dynatest/TestUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | 3 | import java.io.* 4 | import java.nio.file.Files 5 | import java.nio.file.Path 6 | import kotlin.test.expect 7 | 8 | /** 9 | * Expects that [actual] list of objects matches [expected] list of objects. Fails otherwise. 10 | */ 11 | public fun expectList(vararg expected: T, actual: ()->List) { 12 | expect(expected.toList(), actual) 13 | } 14 | 15 | /** 16 | * Expects that [actual] map matches [expected] map, passed in as a list of pairs. Fails otherwise. 17 | */ 18 | public fun expectMap(vararg expected: Pair, actual: ()->Map) { 19 | expect(mapOf(*expected), actual) 20 | } 21 | 22 | /** 23 | * Serializes the object to a byte array 24 | * @return the byte array containing this object serialized form. 25 | */ 26 | public fun Serializable?.serializeToBytes(): ByteArray = ByteArrayOutputStream().also { ObjectOutputStream(it).writeObject(this) }.toByteArray() 27 | 28 | public inline fun ByteArray.deserialize(): T? = T::class.java.cast(ObjectInputStream(inputStream()).readObject()) 29 | 30 | /** 31 | * Clones this object by serialization and returns the deserialized clone. 32 | * @return the clone of this 33 | */ 34 | public fun T.cloneBySerialization(): T = javaClass.cast(serializeToBytes().deserialize()) 35 | 36 | /** 37 | * Similar to [File.deleteRecursively] but throws informative [IOException] instead of 38 | * just returning false on error. uses Java 8 [Files.deleteIfExists] to delete files and folders. 39 | */ 40 | public fun Path.deleteRecursively() { 41 | toFile().walkBottomUp().forEach { Files.deleteIfExists(it.toPath()) } 42 | } 43 | 44 | /** 45 | * Returns the major JVM version of the current JRE, e.g. 6 for Java 1.6, 8 for Java 8, 11 for Java 11 etc. 46 | */ 47 | public val jvmVersion: Int get() = System.getProperty("java.version").parseJvmVersion() 48 | 49 | private fun String.parseJvmVersion(): Int { 50 | // taken from https://stackoverflow.com/questions/2591083/getting-java-version-at-runtime 51 | val version: String = removePrefix("1.").takeWhile { it.isDigit() } 52 | return version.toInt() 53 | } 54 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /dynatest-engine/src/test/kotlin/com/github/mvysny/dynatest/TestSourceUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | 3 | import com.github.mvysny.dynatest.engine.DynaNodeGroupImpl 4 | import com.github.mvysny.dynatest.engine.pretendIsRunningInsideGradle 5 | import com.github.mvysny.dynatest.engine.toTestSource 6 | import org.junit.platform.engine.support.descriptor.ClassSource 7 | import org.junit.platform.engine.support.descriptor.FileSource 8 | import kotlin.test.expect 9 | 10 | class TestSourceUtilsTest : DynaTest({ 11 | 12 | group("tests for StackTraceElement.toTestSource()") { 13 | group("this class resolves to FileSource") { 14 | afterEach { pretendIsRunningInsideGradle = null } 15 | test("gradle") { 16 | pretendIsRunningInsideGradle = true 17 | val e: StackTraceElement = DynaNodeGroupImpl.computeTestSource()!! 18 | val src: ClassSource = e.toTestSource() as ClassSource 19 | expect(TestSourceUtilsTest::class.java.name) { src.className } 20 | expect(e.lineNumber) { src.position.get().line } 21 | } 22 | test("intellij") { 23 | pretendIsRunningInsideGradle = false 24 | val e: StackTraceElement = DynaNodeGroupImpl.computeTestSource()!! 25 | val src: FileSource = e.toTestSource() as FileSource 26 | expect(true, src.file.absolutePath) { 27 | src.file.endsWith("src/test/kotlin/com/github/mvysny/dynatest/TestSourceUtilsTest.kt") 28 | } 29 | expect(e.lineNumber) { src.position.get().line } 30 | } 31 | } 32 | 33 | group("InternalTestingClass resolves to FileSource") { 34 | afterEach { pretendIsRunningInsideGradle = null } 35 | test("gradle") { 36 | pretendIsRunningInsideGradle = true 37 | val e: StackTraceElement = internalTestingClassGetTestSourceOfThis() 38 | val src: ClassSource = e.toTestSource() as ClassSource 39 | expect(InternalTestingClass::class.java.name) { src.className } 40 | expect(e.lineNumber) { src.position.get().line } 41 | } 42 | test("intellij") { 43 | pretendIsRunningInsideGradle = false 44 | val e: StackTraceElement = internalTestingClassGetTestSourceOfThis() 45 | val src: FileSource = e.toTestSource() as FileSource 46 | expect(true, src.file.absolutePath) { 47 | src.file.endsWith("src/main/kotlin/com/github/mvysny/dynatest/InternalTestingClass.kt") 48 | } 49 | expect(12) { src.position.get().line } 50 | } 51 | } 52 | 53 | // a preparation test for gradleFreezingTest(). Generally Gradle freezes if it sees a mixture of FileSource and ClassSource. 54 | // see the gradleFreezingTest() for more info. 55 | test("InternalTestingClass") { 56 | expect(true, InternalTestingClass.getTestSourceOfThis().className) { 57 | InternalTestingClass.getTestSourceOfThis().className.startsWith(InternalTestingClass::class.java.name) 58 | } 59 | } 60 | } 61 | 62 | // A nasty test. This test will make Gradle freeze after last test. 63 | // A Test for https://github.com/gradle/gradle/issues/5737 64 | InternalTestingClass.gradleFreezingTest(this) 65 | }) 66 | -------------------------------------------------------------------------------- /dynatest-api/src/test/kotlin/com/github/mvysny/dynatest/UncompilableTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | 3 | // all of the classes below must not compile: there must be compiler error on every 4 | // line marked with `// compiler error` 5 | 6 | /* 7 | class NestedBeforeEach : DynaTest({ 8 | beforeEach { 9 | beforeEach {} // compiler error 10 | afterEach {} // compiler error 11 | } 12 | beforeEach { 13 | beforeGroup {} // compiler error 14 | afterGroup {} // compiler error 15 | } 16 | }) 17 | 18 | class NestedBeforeGroup : DynaTest({ 19 | beforeGroup { 20 | beforeGroup {} // compiler error 21 | afterGroup {} // compiler error 22 | } 23 | beforeGroup { 24 | beforeEach {} // compiler error 25 | afterEach {} // compiler error 26 | } 27 | }) 28 | 29 | class NestedAfterEach : DynaTest({ 30 | afterEach { 31 | beforeEach {} // compiler error 32 | afterEach {} // compiler error 33 | } 34 | afterEach { 35 | beforeGroup {} // compiler error 36 | afterGroup {} // compiler error 37 | } 38 | }) 39 | 40 | class NestedAfterGroup : DynaTest({ 41 | afterGroup { 42 | beforeGroup {} // compiler error 43 | afterGroup {} // compiler error 44 | } 45 | afterGroup { 46 | beforeEach {} // compiler error 47 | afterEach {} // compiler error 48 | } 49 | }) 50 | 51 | class CallingBeforeFromTest : DynaTest({ 52 | test("foo") { 53 | beforeEach {} // compiler error 54 | beforeGroup {} // compiler error 55 | } 56 | }) 57 | 58 | class CallingAfterFromTest : DynaTest({ 59 | test("foo") { 60 | afterEach {} // compiler error 61 | afterGroup {} // compiler error 62 | } 63 | }) 64 | 65 | class CallingTestFromTest : DynaTest({ 66 | test("foo") { 67 | test("bar") {} // compiler error 68 | xtest("bar") {} // compiler error 69 | } 70 | }) 71 | 72 | class CallingTestFromXTest : DynaTest({ 73 | xtest("foo") { 74 | test("bar") {} // compiler error 75 | xtest("bar") {} // compiler error 76 | } 77 | }) 78 | 79 | class CallingTestFromBefore : DynaTest({ 80 | beforeEach { 81 | test("bar") {} // compiler error 82 | xtest("bar") {} // compiler error 83 | } 84 | beforeGroup { 85 | test("bar") {} // compiler error 86 | xtest("bar") {} // compiler error 87 | } 88 | }) 89 | 90 | class CallingTestFromAfter : DynaTest({ 91 | afterEach { 92 | test("bar") {} // compiler error 93 | xtest("bar") {} // compiler error 94 | } 95 | afterGroup { 96 | test("bar") {} // compiler error 97 | xtest("bar") {} // compiler error 98 | } 99 | }) 100 | 101 | class CallingGroupFromBefore : DynaTest({ 102 | beforeEach { 103 | group("bar") {} // compiler error 104 | xgroup("bar") {} // compiler error 105 | } 106 | beforeGroup { 107 | group("bar") {} // compiler error 108 | xgroup("bar") {} // compiler error 109 | } 110 | }) 111 | 112 | class CallingGroupFromAfter : DynaTest({ 113 | afterEach { 114 | group("bar") {} // compiler error 115 | xgroup("bar") {} // compiler error 116 | } 117 | afterGroup { 118 | group("bar") {} // compiler error 119 | xgroup("bar") {} // compiler error 120 | } 121 | }) 122 | */ 123 | -------------------------------------------------------------------------------- /dynatest/src/test/kotlin/com/github/mvysny/dynatest/TestUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | 3 | import java.io.File 4 | import java.nio.file.Files 5 | import java.util.* 6 | import java.util.concurrent.ConcurrentHashMap 7 | import java.util.concurrent.CopyOnWriteArrayList 8 | import kotlin.test.expect 9 | 10 | class TestUtilsTest : DynaTest({ 11 | group("cloneBySerialization()") { 12 | test("testSimpleObjects") { 13 | expect("a") { "a".cloneBySerialization() } 14 | expect("") { "".cloneBySerialization() } 15 | expect(25) { 25.cloneBySerialization() } 16 | } 17 | } 18 | 19 | group("expectList()") { 20 | test("emptyList") { 21 | expectList() { listOf() } 22 | expectList() { mutableListOf() } 23 | expectList() { LinkedList() } 24 | expectList() { CopyOnWriteArrayList() } 25 | } 26 | 27 | test("singleton list") { 28 | expectList(25) { listOf(25) } 29 | } 30 | 31 | test("simpleListOfStrings") { 32 | expectList("a", "b", "c") { listOf("a", "b", "c") } 33 | } 34 | 35 | test("comparisonFailure") { 36 | expectThrows(AssertionError::class) { 37 | expectList() { listOf("a", "b", "c") } 38 | } 39 | expectThrows(AssertionError::class) { 40 | expectList(1, 2, 3) { listOf("a", "b", "c") } 41 | } 42 | expectThrows(AssertionError::class) { 43 | expectList(1, 2, 3) { listOf() } 44 | } 45 | } 46 | } 47 | 48 | group("expectMap()") { 49 | test("emptyMap") { 50 | expectMap() { mapOf() } 51 | expectMap() { mutableMapOf() } 52 | expectMap() { LinkedHashMap() } 53 | expectMap() { ConcurrentHashMap() } 54 | } 55 | 56 | test("singleton map") { 57 | expectMap(25 to "a") { mapOf(25 to "a") } 58 | } 59 | 60 | test("simpleMapOfStrings") { 61 | expectMap("a" to 1, "b" to 2, "c" to 3) { mutableMapOf("a" to 1, "b" to 2, "c" to 3) } 62 | } 63 | 64 | test("comparisonFailure") { 65 | expectThrows(AssertionError::class) { 66 | expectMap() { mapOf("a" to 1, "b" to 2, "c" to 3) } 67 | } 68 | expectThrows(AssertionError::class) { 69 | expectMap("a" to 1, "b" to 2, "c" to 3) { mapOf(1 to "a", 2 to "b", 3 to "c") } 70 | } 71 | expectThrows(AssertionError::class) { 72 | expectMap("a" to 1, "b" to 2, "c" to 3) { mapOf() } 73 | } 74 | } 75 | } 76 | 77 | group("deleteRecursively()") { 78 | test("simple file") { 79 | val f = File.createTempFile("foo", "bar") 80 | f.expectFile() 81 | f.toPath().deleteRecursively() 82 | f.expectNotExists() 83 | } 84 | test("empty folder") { 85 | val f = Files.createTempDirectory("tmp").toFile() 86 | f.expectDirectory() 87 | f.toPath().deleteRecursively() 88 | f.expectNotExists() 89 | } 90 | test("doesn't fail when the file doesn't exist") { 91 | val f = File.createTempFile("foo", "bar") 92 | f.delete() 93 | f.expectNotExists() 94 | f.toPath().deleteRecursively() 95 | f.expectNotExists() 96 | } 97 | test("non-empty folder") { 98 | val f = Files.createTempDirectory("tmp").toFile() 99 | val foo = File(f, "foo.txt") 100 | foo.writeText("foo") 101 | f.expectDirectory() 102 | f.toPath().deleteRecursively() 103 | foo.expectNotExists() 104 | f.expectNotExists() 105 | } 106 | } 107 | 108 | test("jvmVersion") { 109 | // test that the JVM version parsing doesn't throw 110 | jvmVersion 111 | } 112 | }) 113 | -------------------------------------------------------------------------------- /dynatest-engine/src/test/kotlin/com/github/mvysny/dynatest/GeneralDynaTestEngineTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | 3 | import com.github.mvysny.dynatest.engine.DynaTestEngine 4 | import com.github.mvysny.dynatest.engine.InitFailedTestDescriptor 5 | import org.junit.jupiter.api.Test 6 | import org.junit.platform.engine.* 7 | import org.junit.platform.engine.discovery.ClassSelector 8 | import org.junit.platform.engine.reporting.ReportEntry 9 | import kotlin.test.expect 10 | 11 | /** 12 | * Tests the very general properties and generic error-handling capabilities of the DynaTestEngine itself. More specialized tests are located 13 | * at [DynaTestEngineTest]. 14 | */ 15 | class GeneralDynaTestEngineTest { 16 | /** 17 | * The [TestEngine.discover] function must not fail even if the test discovery itself fails. 18 | */ 19 | @Test 20 | fun failingTestSuiteMustNotFailInDiscover() { 21 | val engine = DynaTestEngine() 22 | withFail { 23 | engine.discover2(TestSuiteFailingInInit::class.java) 24 | } 25 | } 26 | 27 | private fun DynaTestEngine.discover2(vararg testClasses: Class<*>): TestDescriptor { 28 | require (testClasses.isNotEmpty()) 29 | return discover(object : EngineDiscoveryRequest { 30 | override fun getConfigurationParameters(): ConfigurationParameters = EmptyConfigParameters 31 | override fun ?> getFiltersByType(filterType: Class?): MutableList = mutableListOf() 32 | override fun getSelectorsByType(selectorType: Class): MutableList = 33 | testClasses.map { it.toSelector() } .filterIsInstance(selectorType) .toMutableList() 34 | }, UniqueId.forEngine(id)) 35 | } 36 | 37 | /** 38 | * The [TestEngine.discover] block must not fail even if the test discovery itself fails; instead it must produce an always-failing 39 | * test descriptor. 40 | */ 41 | @Test 42 | fun failingTestSuiteMustFailInExecute() { 43 | val engine = DynaTestEngine() 44 | val tests: TestDescriptor = withFail { engine.discover2(TestSuiteFailingInInit::class.java) } 45 | expect>(InitFailedTestDescriptor::class.java) { tests.children.first().javaClass } 46 | expectThrows(RuntimeException::class, "Simulated") { 47 | engine.execute(ExecutionRequest(tests, ThrowingExecutionListener, EmptyConfigParameters)) 48 | } 49 | } 50 | 51 | @Test 52 | fun checkUniqueIDsGeneratedByTheEngine() { 53 | operator fun TestDescriptor.get(index: Int) = children.toList()[index] 54 | 55 | val engine = DynaTestEngine() 56 | var td = engine.discover2(_UniqueIdCheckupClass::class.java) 57 | expect("[engine:DynaTest]") { td.uniqueId.toString() } 58 | expect("[engine:DynaTest]/[group:_UniqueIdCheckupClass]") { td[0].uniqueId.toString() } 59 | td = td[0] 60 | expect("[engine:DynaTest]/[group:_UniqueIdCheckupClass]/[test:root test]") { td[0].uniqueId.toString() } 61 | expect("[engine:DynaTest]/[group:_UniqueIdCheckupClass]/[group:root group]") { td[1].uniqueId.toString() } 62 | expect("[engine:DynaTest]/[group:_UniqueIdCheckupClass]/[group:root group]/[test:nested]") { td[1][0].uniqueId.toString() } 63 | expect("[engine:DynaTest]/[group:_UniqueIdCheckupClass]/[group:root group]/[group:nested group]") { td[1][1].uniqueId.toString() } 64 | expect("[engine:DynaTest]/[group:_UniqueIdCheckupClass]/[group:root group]/[group:nested group]/[test:nested nested]") { td[1][1][0].uniqueId.toString() } 65 | expect("[engine:DynaTest]/[group:_UniqueIdCheckupClass]/[group:root group]/[test:nested2]") { td[1][2].uniqueId.toString() } 66 | expect("[engine:DynaTest]/[group:_UniqueIdCheckupClass]/[test:root test 2]") { td[2].uniqueId.toString() } 67 | } 68 | } 69 | 70 | /** 71 | * An execution listener which immediately throws when an exception occurs. Used together with [runTests] to fail eagerly. 72 | */ 73 | private object ThrowingExecutionListener : EngineExecutionListener { 74 | override fun executionFinished(testDescriptor: TestDescriptor, testExecutionResult: TestExecutionResult) { 75 | if (testExecutionResult.throwable.isPresent) throw testExecutionResult.throwable.get() 76 | } 77 | 78 | override fun reportingEntryPublished(testDescriptor: TestDescriptor, entry: ReportEntry) {} 79 | override fun executionSkipped(testDescriptor: TestDescriptor, reason: String) { 80 | throw RuntimeException("Unexpected") 81 | } 82 | override fun executionStarted(testDescriptor: TestDescriptor) {} 83 | override fun dynamicTestRegistered(testDescriptor: TestDescriptor) { 84 | throw RuntimeException("Unexpected") 85 | } 86 | } 87 | 88 | private fun Class<*>.toSelector(): ClassSelector { 89 | val c = ClassSelector::class.java.declaredConstructors.first { it.parameterTypes[0] == Class::class.java } 90 | c.isAccessible = true 91 | return c.newInstance(this) as ClassSelector 92 | } 93 | 94 | private var fail = false 95 | private fun withFail(block: ()->T): T { 96 | fail = true 97 | try { 98 | return block() 99 | } finally { 100 | fail = false 101 | } 102 | } 103 | 104 | /** 105 | * A dyna test which throws an exception when initialized. Used by [GeneralDynaTestEngineTest] to check that this failure won't make the 106 | * [TestEngine.discover] function fail. 107 | */ 108 | class TestSuiteFailingInInit : DynaTest({ 109 | if (fail) throw RuntimeException("Simulated") 110 | }) 111 | -------------------------------------------------------------------------------- /dynatest-engine/src/test/kotlin/com/github/mvysny/dynatest/DynaRunner.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | 3 | import com.github.mvysny.dynatest.engine.DynaNodeGroupImpl 4 | import com.github.mvysny.dynatest.engine.DynaNodeTestDescriptor 5 | import com.github.mvysny.dynatest.engine.DynaTestEngine 6 | import org.junit.platform.engine.* 7 | import org.junit.platform.engine.reporting.ReportEntry 8 | import java.util.* 9 | import kotlin.test.expect 10 | import kotlin.test.fail 11 | 12 | /** 13 | * A very simple test support, simply runs all registered tests immediately; bails out at first failed test. You generally should rely on 14 | * JUnit5 to run your tests instead - just extend [DynaTest] class. To create a reusable test battery just define an extension method on the 15 | * [DynaNodeGroup] class - see the `CalculatorTest.kt` file for more details. 16 | * @throws TestFailedException if any of the test failed. To expect this, nest call to this function into the [expectFailures] function. 17 | * @return the test statistics results 18 | */ 19 | internal fun runTests(block: DynaNodeGroup.()->Unit): TestResults { 20 | // obtain the test definitions 21 | val group = DynaNodeGroupImpl("root", null, true) 22 | group.block() 23 | group.onDesignPhaseEnd() 24 | 25 | // run the tests using the DynaTestEngine 26 | val testDescriptor = DynaNodeTestDescriptor(UniqueId.forEngine("dynatest"), group) 27 | val result = TestResults() 28 | DynaTestEngine().execute(ExecutionRequest(testDescriptor, 29 | TestResultBuilder(result), 30 | EmptyConfigParameters 31 | )) 32 | if (!result.isSuccess) throw TestFailedException(result) 33 | return result 34 | } 35 | 36 | /** 37 | * Listens on JUnit5 lifecycle. Counts tests which ran, tests which failed, etc... 38 | */ 39 | private class TestResultBuilder(val results: TestResults) : EngineExecutionListener { 40 | override fun executionFinished(testDescriptor: TestDescriptor, testExecutionResult: TestExecutionResult) { 41 | if (testDescriptor is DynaNodeTestDescriptor && testDescriptor.isContainer && testExecutionResult.status == TestExecutionResult.Status.SUCCESSFUL) { 42 | // don't register group as successful, since that would count also groups towards the TestResults.successful count which is confusing. 43 | } else { 44 | results.testsRan[testDescriptor.uniqueId] = testExecutionResult 45 | } 46 | } 47 | 48 | override fun reportingEntryPublished(testDescriptor: TestDescriptor, entry: ReportEntry) {} 49 | 50 | override fun executionSkipped(testDescriptor: TestDescriptor, reason: String) { 51 | results.testsSkipped[testDescriptor.uniqueId] = reason 52 | } 53 | 54 | override fun executionStarted(testDescriptor: TestDescriptor) {} 55 | 56 | override fun dynamicTestRegistered(testDescriptor: TestDescriptor) {} 57 | } 58 | 59 | internal class TestFailedException(val results: TestResults) : Exception(results.toString()) 60 | 61 | /** 62 | * Wraps the [runTests] call and expect it to fail. 63 | */ 64 | internal fun expectFailures(block: ()->Unit, results: TestResults.()->Unit) { 65 | try { 66 | block() 67 | fail("Expected to fail") 68 | } catch (e: TestFailedException) { 69 | e.results.results() 70 | } 71 | } 72 | 73 | /** 74 | * The test results, captured by the [runTests] function. 75 | * @property testsRan all tests that were ran, either successfully or unsuccessfully. Only counts in groups when their `beforeGroup` failed. 76 | * @property testsSkipped typically empty since there is no means to skip a test in DynaTest. 77 | */ 78 | internal data class TestResults(val testsRan: MutableMap = mutableMapOf(), 79 | val testsSkipped: MutableMap = mutableMapOf()) { 80 | 81 | /** 82 | * Number of failed tests and/or groups. 83 | */ 84 | val failures: Int get() = testsRan.values.count { it.status == TestExecutionResult.Status.FAILED } 85 | /** 86 | * Number of successful tests. 87 | */ 88 | val successful: Int get() = testsRan.values.count { it.status == TestExecutionResult.Status.SUCCESSFUL } 89 | val aborted: Int get() = testsRan.values.count { it.status == TestExecutionResult.Status.ABORTED } 90 | val isSuccess: Boolean get() = failures == 0 && aborted == 0 91 | 92 | fun expectStats(successful: Int, failures: Int, aborted: Int) { 93 | expect(successful, dump()) { this.successful } 94 | expect(failures, dump()) { this.failures } 95 | expect(aborted, dump()) { this.aborted } 96 | } 97 | 98 | inline fun expectFailure(name: String) { 99 | expect>(T::class.java) { getFailure(name).javaClass } 100 | } 101 | 102 | fun getFailure(name: String): Throwable { 103 | val entry = testsRan.entries.firstOrNull { it.key.segments.last().value == name } ?: throw IllegalArgumentException("No test with name '$name': ${testsRan.keys}") 104 | expect(TestExecutionResult.Status.FAILED) { entry.value.status } 105 | return entry.value.throwable.get() 106 | } 107 | 108 | override fun toString() = "TestResults(successful=$successful, failures=$failures, aborted=$aborted, skipped=${testsSkipped.size})" 109 | 110 | /** 111 | * Dumps all failures and their stacktraces; then dumps the test overview. 112 | */ 113 | fun dump() = buildString { 114 | testsRan.entries.filter { it.value.status != TestExecutionResult.Status.SUCCESSFUL } .forEach { 115 | append("${it.key} --> ${it.value.status}\n") 116 | if (it.value.throwable.isPresent) { 117 | append(it.value.throwable.get().getStackTraceAsString()) 118 | } 119 | append('\n') 120 | } 121 | testsSkipped.forEach { 122 | append("${it.key} --> SKIPPED: ${it.value}\n") 123 | } 124 | append(this@TestResults) 125 | } 126 | } 127 | 128 | internal object EmptyConfigParameters : ConfigurationParameters { 129 | override fun getBoolean(key: String?): Optional = Optional.ofNullable(null) 130 | @Deprecated("Deprecated in Java") 131 | override fun size(): Int = 0 132 | override fun keySet(): MutableSet = mutableSetOf() 133 | override fun get(key: String?): Optional = Optional.ofNullable(null) 134 | } 135 | -------------------------------------------------------------------------------- /dynatest-engine/src/main/kotlin/com/github/mvysny/dynatest/engine/DynaNodeImpl.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest.engine 2 | 3 | import com.github.mvysny.dynatest.* 4 | import org.junit.platform.engine.TestSource 5 | 6 | /** 7 | * A definition of a test graph node, either a group or a concrete test. Since we can't run tests right when [DynaNodeGroup.test] 8 | * is called (because it's the job of JUnit5 to actually run tests), we need to remember the test so that we can tell JUnit5 to run it 9 | * later on. 10 | * 11 | * Every [DynaNodeGroup.test] and [DynaNodeGroup.group] call 12 | * creates this node which in turn can be converted to JUnit5 structures eligible for execution. 13 | * @property name the test/group name, as passed to the [DynaNodeGroup.test]/[DynaNodeGroup.group] function. 14 | * @property src points to the source file which called [DynaNodeGroup.test]/[DynaNodeGroup.group] function. Used to construct [toTestSource]. 15 | * @property enabled whether this group/test is enabled. Usually `true` unless [DynaNodeGroup.xtest] and friends are used. 16 | */ 17 | internal sealed class DynaNodeImpl( 18 | internal val name: String, 19 | internal val src: StackTraceElement?, 20 | internal val enabled: Boolean 21 | ) { 22 | /** 23 | * Returns a JUnit pointer towards the source of this test method/group. Calculated from [src]. 24 | */ 25 | abstract fun toTestSource(): TestSource? 26 | } 27 | 28 | /** 29 | * Represents a single test with a [name] and the test's [body]. Created when you call [DynaNodeGroup.test]. 30 | * 31 | * To start writing tests, just extend [DynaTest]. See [DynaTest] for more details. 32 | */ 33 | internal class DynaNodeTestImpl internal constructor( 34 | name: String, 35 | internal val body: DynaNodeTest.() -> Unit, 36 | src: StackTraceElement?, 37 | enabled: Boolean 38 | ) : DynaNodeImpl(name, src, enabled), DynaNodeTest { 39 | override fun toTestSource(): TestSource? = src?.toTestSource(name) 40 | } 41 | 42 | /** 43 | * Represents a single test group with a [name]. Created when you call [group]. 44 | * 45 | * To start writing tests, just extend [DynaTest]. See [DynaTest] for more details. 46 | */ 47 | internal class DynaNodeGroupImpl internal constructor( 48 | name: String, 49 | src: StackTraceElement?, 50 | enabled: Boolean 51 | ) : DynaNodeImpl(name, src, enabled), DynaNodeGroup { 52 | 53 | override fun toTestSource(): TestSource? = src?.toTestSource(null) 54 | 55 | private var inDesignPhase: Boolean = true 56 | private fun checkInDesignPhase(funName: String) { 57 | check(inDesignPhase) { "It appears that you are attempting to call $funName from a test{} block. You should create tests only from the group{} blocks since they run at design time (and not at run time, like the test{} blocks)" } 58 | } 59 | 60 | internal val children = mutableListOf() 61 | /** 62 | * What to run before every test. 63 | */ 64 | internal val beforeEach = mutableListOfUnit>() 65 | /** 66 | * What to run after every test. 67 | */ 68 | internal val afterEach = mutableListOfUnit>() 69 | /** 70 | * What to run before any of the test is started in this group. 71 | */ 72 | internal val beforeGroup = mutableListOfUnit>() 73 | /** 74 | * What to run after all tests are done in this group. 75 | */ 76 | internal val afterGroup = mutableListOfUnit>() 77 | 78 | internal fun onDesignPhaseEnd() { 79 | inDesignPhase = false 80 | children.forEach { (it as? DynaNodeGroupImpl)?.onDesignPhaseEnd() } 81 | } 82 | 83 | private fun checkNameNotYetUsed(name: String) { 84 | require(children.none { it.name == name }) { "test/group with name '$name' is already present: ${children.joinToString { it.name }}" } 85 | } 86 | 87 | override fun test(name: String, body: DynaNodeTest.()->Unit) { 88 | test(name, body, true) 89 | } 90 | 91 | private fun test(name: String, body: DynaNodeTest.()->Unit, enabled: Boolean) { 92 | checkInDesignPhase("test") 93 | checkNameNotYetUsed(name) 94 | val source = computeTestSource() 95 | children.add(DynaNodeTestImpl(name, body, source, this.enabled && enabled)) 96 | } 97 | 98 | override fun group(name: String, block: DynaNodeGroup.()->Unit) { 99 | group(name, block, true) 100 | } 101 | 102 | private fun group(name: String, block: DynaNodeGroup.()->Unit, enabled: Boolean) { 103 | checkInDesignPhase("group") 104 | checkNameNotYetUsed(name) 105 | val source = computeTestSource() 106 | val group = DynaNodeGroupImpl(name, source, this.enabled && enabled) 107 | group.block() 108 | children.add(group) 109 | } 110 | 111 | override fun xtest(name: String, body: DynaNodeTest.() -> Unit) { 112 | test(name, body, false) 113 | } 114 | 115 | override fun xgroup(name: String, block: DynaNodeGroup.() -> Unit) { 116 | group(name, block, false) 117 | } 118 | 119 | override fun beforeEach(block: (@DynaTestDsl Unit).()->Unit) { 120 | checkInDesignPhase("beforeEach") 121 | beforeEach.add(block) 122 | } 123 | 124 | override fun afterEach(block: (@DynaTestDsl Unit).(Outcome)->Unit) { 125 | checkInDesignPhase("afterEach") 126 | afterEach.add(block) 127 | } 128 | 129 | override fun beforeGroup(block: (@DynaTestDsl Unit).()->Unit) { 130 | checkInDesignPhase("beforeGroup") 131 | beforeGroup.add(block) 132 | } 133 | 134 | override fun afterGroup(block: (@DynaTestDsl Unit).(Outcome)->Unit) { 135 | checkInDesignPhase("afterGroup") 136 | afterGroup.add(block) 137 | } 138 | 139 | companion object { 140 | private val pkg: String = DynaNodeGroupImpl::class.java.`package`.name 141 | /** 142 | * Computes the pointer to the source of the test and returns it. 143 | * @return the pointer to the test source; returns null if the source can not be computed by any means. 144 | */ 145 | internal fun computeTestSource(): StackTraceElement? { 146 | val stackTrace: Array = Thread.currentThread().stackTrace 147 | // find first stack trace which doesn't point to this package and is not Thread.getStackTrace() 148 | // That's going to be the caller of the test/group method. 149 | return stackTrace.asSequence() 150 | .filter { !it.className.startsWith(pkg) && it.className != Thread::class.java.name } 151 | .firstOrNull() 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /dynatest/src/test/kotlin/com/github/mvysny/dynatest/FileTestUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | 3 | import java.io.File 4 | import java.nio.file.Files 5 | import kotlin.properties.ReadWriteProperty 6 | import kotlin.test.expect 7 | 8 | class FileTestUtilsTest : DynaTest({ 9 | group("expectExists()") { 10 | test("passes on existing file") { 11 | File.createTempFile("foooo", "bar").expectExists() 12 | } 13 | 14 | test("passes on existing dir") { 15 | Files.createTempDirectory("foo").expectExists() 16 | } 17 | 18 | test("fails on nonexisting file") { 19 | expectThrows(AssertionError::class, "${slash}non${slash}existing does not exist") { 20 | File("/non/existing").expectExists() 21 | } 22 | } 23 | } 24 | 25 | group("expectNotExists()") { 26 | test("fails on existing file") { 27 | expectThrows(AssertionError::class, "bar exists") { 28 | File.createTempFile("foooo", "bar").expectNotExists() 29 | } 30 | } 31 | 32 | test("fails on existing dir") { 33 | expectThrows(AssertionError::class, " exists") { 34 | Files.createTempDirectory("foo").expectNotExists() 35 | } 36 | } 37 | 38 | test("passes on nonexisting file") { 39 | File("/non/existing").expectNotExists() 40 | } 41 | } 42 | 43 | group("expectFile()") { 44 | test("passes on existing file") { 45 | File.createTempFile("foooo", "bar").expectFile() 46 | } 47 | 48 | test("fails on existing dir") { 49 | expectThrows(AssertionError::class, " is not a file") { 50 | Files.createTempDirectory("foo").expectFile() 51 | } 52 | } 53 | 54 | test("fails on nonexisting file") { 55 | expectThrows(AssertionError::class, "${slash}non${slash}existing does not exist") { 56 | File("/non/existing").expectFile() 57 | } 58 | } 59 | } 60 | 61 | group("expectDirectory()") { 62 | test("fails on existing file") { 63 | expectThrows(AssertionError::class, "bar is not a directory") { 64 | File.createTempFile("foooo", "bar").expectDirectory() 65 | } 66 | } 67 | 68 | test("passes on existing dir") { 69 | Files.createTempDirectory("foo").expectDirectory() 70 | } 71 | 72 | test("fails on nonexisting file") { 73 | expectThrows(AssertionError::class, "${slash}non${slash}existing does not exist") { 74 | File("/non/existing").expectDirectory() 75 | } 76 | } 77 | } 78 | 79 | group("expectReadableFile()") { 80 | test("passes on existing file") { 81 | File.createTempFile("foooo", "bar").expectReadableFile() 82 | } 83 | 84 | test("fails on existing dir") { 85 | expectThrows(AssertionError::class, " is not a file") { 86 | Files.createTempDirectory("foo").expectReadableFile() 87 | } 88 | } 89 | 90 | test("fails on nonexisting file") { 91 | expectThrows(AssertionError::class, "${slash}non${slash}existing does not exist") { 92 | File("/non/existing").expectReadableFile() 93 | } 94 | } 95 | 96 | if (!OsUtils.isWindows) { 97 | // this test doesn't work on Windows 98 | test("fails on unreadable file") { 99 | expectThrows(AssertionError::class, " is not readable") { 100 | val f = File.createTempFile("foooo", "bar") 101 | f.setReadable(false) 102 | f.expectReadableFile() 103 | } 104 | } 105 | } 106 | 107 | test("succeeds on read-only file") { 108 | val f = File.createTempFile("foooo", "bar") 109 | f.setWritable(false) 110 | f.expectReadableFile() 111 | } 112 | } 113 | 114 | group("expectWritableFile()") { 115 | test("passes on existing file") { 116 | File.createTempFile("foooo", "bar").expectWritableFile() 117 | } 118 | 119 | test("fails on existing dir") { 120 | expectThrows(AssertionError::class, " is not a file") { 121 | Files.createTempDirectory("foo").expectWritableFile() 122 | } 123 | } 124 | 125 | test("fails on nonexisting file") { 126 | expectThrows(AssertionError::class, "${slash}non${slash}existing does not exist") { 127 | File("/non/existing").expectWritableFile() 128 | } 129 | } 130 | 131 | test("succeeds on unreadable file") { 132 | val f = File.createTempFile("foooo", "bar") 133 | f.setReadable(false) 134 | f.expectWritableFile() 135 | } 136 | 137 | test("fails on read-only file") { 138 | expectThrows(AssertionError::class, " is not writable") { 139 | val f = File.createTempFile("foooo", "bar") 140 | f.setWritable(false) 141 | f.expectWritableFile() 142 | } 143 | } 144 | } 145 | 146 | group("withTempDir()") { 147 | 148 | // a demo of a function which uses `withTempDir` and populates/inits the folder further. 149 | @DynaTestDsl 150 | fun DynaNodeGroup.reusable(): ReadWriteProperty = 151 | withTempDir("sources") { dir -> File(dir, "foo.txt").writeText("") } 152 | 153 | group("simple") { 154 | val tempDir: File by withTempDir() 155 | lateinit var file: File 156 | beforeEach { 157 | // expect that the folder already exists, so that we can e.g. copy stuff there 158 | tempDir.expectDirectory() 159 | file = File(tempDir, "foo.txt") // example contents 160 | file.writeText("") 161 | } 162 | test("temp dir checker") { 163 | tempDir.expectDirectory() 164 | tempDir.expectFiles("**/*.txt") 165 | file.expectReadableFile() 166 | } 167 | } 168 | // tests the 'reusable' approach where the developer doesn't call `withTempDir()` directly 169 | // but creates a reusable function. 170 | group("reusable") { 171 | val tempDir: File by reusable() 172 | test("txt file checker") { 173 | tempDir.expectFiles("**/*.txt") 174 | } 175 | } 176 | group("deletes temp folder afterwards") { 177 | val tempDir: File by withTempDir() 178 | afterEach { 179 | expect(false) { tempDir.exists() } 180 | } 181 | test("dummy") {} 182 | } 183 | } 184 | }) 185 | 186 | val slash: Char = File.separatorChar 187 | -------------------------------------------------------------------------------- /dynatest-engine/src/test/kotlin/com/github/mvysny/dynatest/TestUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | 3 | import com.github.mvysny.dynatest.engine.DynaNodeGroupImpl 4 | import com.github.mvysny.dynatest.engine.isRunningInsideGradle 5 | import com.github.mvysny.dynatest.engine.toTestSource 6 | import org.junit.platform.engine.support.descriptor.ClassSource 7 | import org.junit.platform.engine.support.descriptor.FileSource 8 | import java.io.IOException 9 | import kotlin.test.expect 10 | 11 | class TestUtilsTest : DynaTest({ 12 | group("expectThrows()") { 13 | test("throwing expected exception succeeds") { 14 | expectThrows(RuntimeException::class) { throw RuntimeException("Expected") } 15 | } 16 | 17 | test("fails if block completes successfully") { 18 | try { 19 | expectThrows(RuntimeException::class) {} // expected to be failed with AssertionError 20 | throw RuntimeException("Should have failed") 21 | } catch (e: AssertionError) { 22 | // okay 23 | expect("Expected an exception of class java.lang.RuntimeException to be thrown, but was completed successfully.") { e.message } 24 | expect(null) { e.cause } 25 | } 26 | } 27 | 28 | test("fails if block throws something else") { 29 | try { 30 | // this should fail with AssertionError since some other exception has been thrown 31 | expectThrows(RuntimeException::class) { 32 | throw IOException("simulated") 33 | } 34 | throw RuntimeException("Should have failed") 35 | } catch (e: AssertionError) { 36 | // okay 37 | expect("Expected an exception of class java.lang.RuntimeException to be thrown, but was java.io.IOException: simulated") { e.message } 38 | expect?>(IOException::class.java) { e.cause?.javaClass } 39 | } 40 | } 41 | 42 | group("message") { 43 | test("throwing expected exception succeeds") { 44 | expectThrows(RuntimeException::class, "Expected") { throw RuntimeException("Expected") } 45 | } 46 | 47 | test("fails if the message is different") { 48 | try { 49 | expectThrows(RuntimeException::class, "foo") { throw RuntimeException("actual") } 50 | throw RuntimeException("Should have failed") 51 | } catch (e: AssertionError) { 52 | // expected 53 | expect("java.lang.RuntimeException message: Expected 'foo' but was 'actual'") { e.message } 54 | } 55 | } 56 | 57 | test("fails if block completes successfully") { 58 | try { 59 | expectThrows(RuntimeException::class, "foo") {} // expected to be failed with AssertionError 60 | throw RuntimeException("Should have failed") 61 | } catch (e: AssertionError) { 62 | // okay 63 | expect("Expected an exception of class java.lang.RuntimeException to be thrown, but was completed successfully.") { e.message } 64 | } 65 | } 66 | 67 | test("fails if block throws something else") { 68 | expectThrows(AssertionError::class, "Expected an exception of class java.lang.RuntimeException to be thrown, but was java.io.IOException: simulated") { 69 | // this should fail with AssertionError since some other exception has been thrown 70 | expectThrows(RuntimeException::class, "simulated") { 71 | throw IOException("simulated") 72 | } 73 | } 74 | } 75 | 76 | test("thrown exception attached as cause to the AssertionError") { 77 | try { 78 | expectThrows(IOException::class, "foo") { 79 | throw IOException("simulated") 80 | } 81 | throw RuntimeException("Should have failed") 82 | } catch (e: AssertionError) { 83 | // okay 84 | expect("java.io.IOException message: Expected 'foo' but was 'simulated'") { e.message } 85 | expect>(IOException::class.java) { e.cause!!.javaClass } 86 | } 87 | } 88 | } 89 | 90 | group("AssertionError not handled specially") { 91 | test("throwing expected exception succeeds") { 92 | expectThrows(AssertionError::class) { throw AssertionError("Expected") } 93 | } 94 | 95 | test("fails if block completes successfully") { 96 | try { 97 | expectThrows(AssertionError::class) {} 98 | throw RuntimeException("Should have failed") 99 | } catch (e: AssertionError) { /*okay*/ } 100 | } 101 | 102 | test("fails if block throws something else") { 103 | try { 104 | // this should fail with AssertionError since some other exception has been thrown 105 | expectThrows(AssertionError::class) { 106 | throw IOException("simulated") 107 | } 108 | throw RuntimeException("Should have failed") 109 | } catch (e: AssertionError) { /*okay*/ } 110 | } 111 | 112 | test("fails on unexpected message") { 113 | try { 114 | // this should fail with AssertionError since some other exception has been thrown 115 | expectThrows(IOException::class, "expected") { 116 | throw IOException("simulated") 117 | } 118 | throw RuntimeException("Should have failed") 119 | } catch (e: AssertionError) { /*okay*/ } 120 | } 121 | } 122 | } 123 | 124 | group("tests for StackTraceElement.toTestSource()") { 125 | test("this class resolves to FileSource") { 126 | val e: StackTraceElement = DynaNodeGroupImpl.computeTestSource()!! 127 | if (isRunningInsideGradle) { 128 | val src: ClassSource = e.toTestSource() as ClassSource 129 | expect(src.className) { src.className } 130 | expect(e.lineNumber) { src.position.get().line } 131 | } else { 132 | val src = e.toTestSource() as FileSource 133 | expect( 134 | true, 135 | src.file.absolutePath 136 | ) { src.file.absolutePath.endsWith("src/test/kotlin/com/github/mvysny/dynatest/TestUtilsTest.kt") } 137 | expect(e.lineNumber) { src.position.get().line } 138 | } 139 | } 140 | } 141 | }) 142 | -------------------------------------------------------------------------------- /dynatest/src/main/kotlin/com/github/mvysny/dynatest/FileTestUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | 3 | import java.io.File 4 | import java.nio.file.FileSystems 5 | import java.nio.file.Files 6 | import java.nio.file.Path 7 | import java.nio.file.PathMatcher 8 | import kotlin.properties.ReadWriteProperty 9 | import kotlin.test.expect 10 | import kotlin.test.fail 11 | 12 | /** 13 | * Expects that this file or directory exists on the file system. 14 | */ 15 | public fun File.expectExists() { 16 | expect(true, "file $absoluteFile does not exist") { exists() } 17 | } 18 | 19 | /** 20 | * Expects that this file or directory exists on the file system. 21 | */ 22 | public fun Path.expectExists() { 23 | toFile().expectExists() 24 | } 25 | 26 | /** 27 | * Expects that this file or directory does not exist on the file system. 28 | */ 29 | public fun File.expectNotExists() { 30 | expect(false, "file/dir $absoluteFile exists") { exists() } 31 | } 32 | 33 | /** 34 | * Expects that this file or directory does not exist on the file system. 35 | */ 36 | public fun Path.expectNotExists() { 37 | toFile().expectNotExists() 38 | } 39 | 40 | /** 41 | * Expects that this file or directory [expectExists] and is a directory. 42 | */ 43 | public fun File.expectDirectory() { 44 | expectExists() 45 | expect(true, "file $absoluteFile is not a directory") { isDirectory } 46 | } 47 | 48 | /** 49 | * Expects that this file or directory [expectExists] and is a directory. 50 | */ 51 | public fun Path.expectDirectory() { 52 | toFile().expectDirectory() 53 | } 54 | 55 | /** 56 | * Expects that this file or directory [expectExists] and is a file. 57 | */ 58 | public fun File.expectFile() { 59 | expectExists() 60 | expect(true, "file $absoluteFile is not a file") { isFile } 61 | } 62 | 63 | /** 64 | * Expects that this file or directory [expectExists] and is a file. 65 | */ 66 | public fun Path.expectFile() { 67 | toFile().expectFile() 68 | } 69 | 70 | /** 71 | * Expects that this file or directory is a file [expectFile] and is readable ([File.canRead]). 72 | */ 73 | public fun File.expectReadableFile() { 74 | expectFile() 75 | expect(true, "file $absoluteFile is not readable") { canRead() } 76 | } 77 | 78 | /** 79 | * Expects that this file or directory is a file [expectFile] and is readable ([File.canRead]). 80 | */ 81 | public fun Path.expectReadableFile() { 82 | toFile().expectReadableFile() 83 | } 84 | 85 | /** 86 | * Expects that this file or directory is a file [expectFile] and is readable ([File.canRead]). 87 | */ 88 | public fun File.expectWritableFile() { 89 | expectFile() 90 | expect(true, "file $absoluteFile is not writable") { canWrite() } 91 | } 92 | 93 | /** 94 | * Expects that this file or directory is a file [expectFile] and is readable ([File.canRead]). 95 | */ 96 | public fun Path.expectWritableFile() { 97 | toFile().expectWritableFile() 98 | } 99 | 100 | /** 101 | * Configures current [DynaNodeGroup] to create a temporary folder before every test is run, then delete it afterwards. 102 | * 103 | * Usage: 104 | * ```kotlin 105 | * group("source generator tests") { 106 | * val sources: File by withTempDir("sources") 107 | * test("simple") { 108 | * generateSourcesTo(sources) 109 | * val generatedFiles: List = sources.expectFiles("*.java", 10..10) 110 | * // ... 111 | * } 112 | * test("more complex test") { 113 | * // 'sources' will point to a new temporary directory now. 114 | * generateSourcesTo(sources) 115 | * // ... 116 | * } 117 | * } 118 | * ``` 119 | * 120 | * To create a reusable utility function which e.g. pre-populates the directory, you have 121 | * to use a different syntax: 122 | * 123 | * ```kotlin 124 | * fun DynaNodeGroup.withSources(): ReadWriteProperty { 125 | * val sourcesProperty: ReadWriteProperty = withTempDir("sources") 126 | * val sources by sourcesProperty 127 | * beforeEach { 128 | * generateSourcesTo(sources) 129 | * } 130 | * return sourcesProperty 131 | * } 132 | * 133 | * group("source generator tests") { 134 | * val sources: File by withSources() 135 | * test("simple") { 136 | * val generatedFiles: List = sources.expectFiles("*.java", 10..10) 137 | * } 138 | * } 139 | * ``` 140 | * Make sure to never return `sources` since that would query the value of the `sourcesProperty` 141 | * right away, failing with `unitialized` `RuntimeException`. 142 | * @param name an optional tempdir name as passed into [createTempDir]. 143 | * Defaults to "dir". 144 | * @param keepOnFailure if true (default), the directory is not deleted on test failure so that you 145 | * can take a look what went wrong. Set this to `false` to always delete the directory. 146 | * @param init called right after the directory has been created, before any tests are executed. 147 | * Allows you to for example populate the temp folder with some test files. 148 | */ 149 | @DynaTestDsl 150 | public fun DynaNodeGroup.withTempDir( 151 | name: String = "dir", 152 | keepOnFailure: Boolean = true, 153 | init: (File) -> Unit = {} 154 | ): ReadWriteProperty { 155 | // don't use a Provider class with 'public operator fun provideDelegate' since it makes it really 156 | // hard to wrap `withTempDir()` in another utility function. 157 | 158 | val property = LateinitProperty(name) 159 | var dir: File by property 160 | beforeEach { 161 | dir = Files.createTempDirectory("tmp-$name").toFile() 162 | init(dir) 163 | } 164 | afterEach { outcome: Outcome -> 165 | if (!keepOnFailure || outcome.isSuccess) { 166 | dir.toPath().deleteRecursively() 167 | } else { 168 | println("Test ${outcome.testName} failed, keeping temporary dir $property") 169 | } 170 | } 171 | return property 172 | } 173 | 174 | /** 175 | * Finds all files matching given [glob] pattern in this directory. 176 | * Always pass in forward slashes as path separators, even on Windows. 177 | * @param glob the files to find, e.g. `libs/ *.war` (only in particular folder) or `** / *.java` or `**.java` (anywhere). 178 | * @param expectedCount expected number of files, defaults to 1. 179 | */ 180 | public fun File.expectFiles(glob: String, expectedCount: IntRange = 1..1): List { 181 | expectDirectory() 182 | 183 | // common mistake: **/*.java wouldn't match files in root folder. 184 | @Suppress("NAME_SHADOWING") 185 | val glob: String = glob.replace("**/", "**") 186 | val pattern: String = if (OsUtils.isWindows) { 187 | // replace \ with \\ to avoid collapsing; replace forward slashes in glob with \\ 188 | "glob:$absolutePath".replace("""\""", """\\""") + """\\""" + glob.replace("/", """\\""") 189 | } else { 190 | "glob:$absolutePath/$glob" 191 | } 192 | val matcher: PathMatcher = FileSystems.getDefault().getPathMatcher(pattern) 193 | val found: List = absoluteFile.walk() 194 | .filter { matcher.matches(it.toPath()) } 195 | .toList() 196 | if (found.size !in expectedCount) { 197 | fail("Expected $expectedCount $glob but found ${found.size}: $found . Folder dump: ${absoluteFile.walk().joinToString("\n")}") 198 | } 199 | return found 200 | } 201 | 202 | /** 203 | * Operating system-related utilities. 204 | */ 205 | internal object OsUtils { 206 | val osName: String = System.getProperty("os.name") 207 | /** 208 | * True if we're running on Windows, false on Linux, Mac and others. 209 | */ 210 | val isWindows: Boolean get() = osName.startsWith("Windows") 211 | } 212 | -------------------------------------------------------------------------------- /dynatest-api/src/main/kotlin/com/github/mvysny/dynatest/DynaNode.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | 3 | /** 4 | * Makes sure that you can't call [DynaNodeGroup] methods from the scope of the [DynaNodeTest]. 5 | */ 6 | @Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE, AnnotationTarget.FUNCTION) 7 | @DslMarker 8 | public annotation class DynaTestDsl 9 | 10 | @Deprecated("Replace by DynaTestDsl", ReplaceWith("DynaTestDsl")) 11 | public typealias DynaNodeDsl = DynaTestDsl 12 | 13 | /** 14 | * A group of tests, may contain tests and other groups as well. Created when you call [group]. Allows you to control test 15 | * lifecycle: 16 | * * [beforeEach]/[afterEach] adds action that need to be executed before/after every test, in this group and all subgroups 17 | * * [beforeGroup]/[afterGroup] adds action that need to be executed before/after all tests in this group and all subgroups are ran 18 | * 19 | * To start writing tests, just extend the `DynaTest` class in dynatest-engine. See `DynaTest` class documentation for more details. 20 | * 21 | * *Warning*: the methods may only be called when the `DynaTest` class is constructed. None of these methods can be called when the 22 | * tests are being run. Doing so will throw [IllegalStateException]. 23 | * 24 | * **Implementation note:** The weird `(@DynaNodeDsl Unit)` receiver is only used to prevent 25 | * calling e.g. `test {}` from `beforeGroup {}`. Whenever messing around with the DSL annotations, 26 | * please make sure to look at the `UncompilableTest.kt` file. 27 | */ 28 | @DynaTestDsl 29 | public interface DynaNodeGroup { 30 | /** 31 | * Creates a new test case with given [name] and registers it within current group. Does not run the test closure immediately - 32 | * the test is only registered for being run later on by JUnit5 runner. 33 | * @param body the implementation of the test; does not run immediately but only when the test case is run 34 | * @throws IllegalStateException if this method is called when the tests are being run by JUnit. 35 | */ 36 | @DynaTestDsl 37 | public fun test(name: String, body: DynaNodeTest.() -> Unit) 38 | 39 | /** 40 | * Creates a nested group with given [name] and runs given [block]. In the block, you can create both sub-groups and tests, and you can 41 | * mix those freely as you like. 42 | * @param block the block, runs immediately. 43 | * @throws IllegalStateException if this method is called when the tests are being run by JUnit. 44 | */ 45 | @DynaTestDsl 46 | public fun group(name: String, block: DynaNodeGroup.() -> Unit) 47 | 48 | /** 49 | * Creates a disabled test case with given [name] and registers it within current group. Does not run the test closure - 50 | * the test is only registered but not run later on by JUnit5 runner. 51 | * 52 | * Useful for temporarily disabling a test. 53 | * @param body the implementation of the test; does not run immediately but only when the test case is run 54 | * @throws IllegalStateException if this method is called when the tests are being run by JUnit. 55 | */ 56 | @DynaTestDsl 57 | public fun xtest(name: String, body: DynaNodeTest.() -> Unit) 58 | 59 | /** 60 | * Creates a nested disabled group with given [name] and runs given [block]. 61 | * In the block, you can create both sub-groups and tests, and you can 62 | * mix those freely as you like. 63 | * 64 | * None of the child tests are run. Useful for temporarily disabling a group of tests. 65 | * @param block the block, runs immediately. 66 | * @throws IllegalStateException if this method is called when the tests are being run by JUnit. 67 | */ 68 | @DynaTestDsl 69 | public fun xgroup(name: String, block: DynaNodeGroup.() -> Unit) 70 | 71 | /** 72 | * Registers a block which will be run exactly once before any of the tests in the current group are run. 73 | * Only the tests nested in this group and its subgroups are 74 | * considered. 75 | * 76 | * If this group is enabled, these blocks will be run regardless of whether there 77 | * are any child tests/groups in this group or not. 78 | * @param block the block to run. Any exceptions thrown by the block will make the test fail. 79 | * @throws IllegalStateException if this method is called when the tests are being run by JUnit. 80 | */ 81 | @DynaTestDsl 82 | public fun beforeGroup(block: (@DynaTestDsl Unit).() -> Unit) 83 | 84 | /** 85 | * Registers a block which will be run before every test registered to this group and to any nested groups. 86 | * `beforeEach` blocks registered by a parent/ancestor group runs before `beforeEach` blocks registered by this group. 87 | * 88 | * If any of the `beforeEach` blocks fails, no further `beforeEach` blocks are executed; furthermore the test itself is not executed as well. 89 | * However, all of the [afterEach] blocks for the corresponding group and all parent groups still *are* executed. 90 | * 91 | * If this test or any parent group is disabled, these blocks will not run. 92 | * @param block the block to run. Any exceptions thrown by the block will make the test fail. 93 | * @throws IllegalStateException if this method is called when the tests are being run by JUnit. 94 | */ 95 | @DynaTestDsl 96 | public fun beforeEach(block: (@DynaTestDsl Unit).() -> Unit) 97 | 98 | /** 99 | * Registers a [block] which will be run after every test registered to this group and to any nested groups. 100 | * All `afterEach` blocks registered by a parent/ancestor group runs after `afterEach` blocks registered by this group. 101 | * 102 | * The `afterEach` blocks are called even if the test fails. If the `beforeEach` block fails, only the `afterEach` blocks in the corresponding 103 | * group and all ancestor groups are called. 104 | * 105 | * If any of the `afterEach` blocks throws an exception, those exceptions are added as [Throwable.getSuppressed] to the main exception (as thrown 106 | * by the `beforeEach` block or the test itself); or just rethrown if there is no main exception. Any exception thrown by the `afterEach` 107 | * block will cause the test to fail. 108 | * 109 | * If this test or any parent group is disabled, these blocks will not run. 110 | * @param block the block to run. Any exceptions thrown by the block will make the test fail. 111 | * The block receives an [Outcome] of the test run. 112 | * @throws IllegalStateException if this method is called when the tests are being run by JUnit. 113 | */ 114 | @DynaTestDsl 115 | public fun afterEach(block: (@DynaTestDsl Unit).(outcome: @DynaTestDsl Outcome) -> Unit) 116 | 117 | /** 118 | * Registers a block which will be run only once after all of the tests are run in the current group. Only the tests nested in this group and its subgroups are 119 | * considered. 120 | * 121 | * If this group is enabled, these blocks will be run regardless of whether there 122 | * are any child tests/groups in this group or not. 123 | * @param block the block to run. Any exceptions thrown by the block will make the test fail. 124 | * The block receives an [Outcome] of the test run. 125 | * @throws IllegalStateException if this method is called when the tests are being run by JUnit. 126 | */ 127 | @DynaTestDsl 128 | public fun afterGroup(block: (@DynaTestDsl Unit).(outcome: @DynaTestDsl Outcome) -> Unit) 129 | } 130 | 131 | /** 132 | * Represents a single test with a name and a body block. Created when you call [DynaNodeGroup.test]. 133 | * 134 | * To start writing tests, just extend the `DynaTest` class located in the `dynatest-engine` module. See `DynaTest` for more details. 135 | * 136 | * The main purpose of this interface is to serve as a DSL marker, to prevent 137 | * calling e.g. `test {}` from `beforeGroup {}`. 138 | */ 139 | @DynaTestDsl 140 | public interface DynaNodeTest 141 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Stop when "xargs" is not available. 209 | if ! command -v xargs >/dev/null 2>&1 210 | then 211 | die "xargs is not available" 212 | fi 213 | 214 | # Use "xargs" to parse quoted args. 215 | # 216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 217 | # 218 | # In Bash we could simply go: 219 | # 220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 221 | # set -- "${ARGS[@]}" "$@" 222 | # 223 | # but POSIX shell has neither arrays nor command substitution, so instead we 224 | # post-process each arg (as a line of input to sed) to backslash-escape any 225 | # character that might be a shell metacharacter, then use eval to reverse 226 | # that process (while maintaining the separation between arguments), and wrap 227 | # the whole thing up as a single "set" statement. 228 | # 229 | # This will of course break if any of these variables contains a newline or 230 | # an unmatched quote. 231 | # 232 | 233 | eval "set -- $( 234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 235 | xargs -n1 | 236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 237 | tr '\n' ' ' 238 | )" '"$@"' 239 | 240 | exec "$JAVACMD" "$@" 241 | -------------------------------------------------------------------------------- /dynatest-engine/src/main/kotlin/com/github/mvysny/dynatest/engine/TestSourceUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest.engine 2 | 3 | import com.github.mvysny.dynatest.InternalTestingClass 4 | import org.junit.platform.commons.util.ReflectionUtils 5 | import org.junit.platform.engine.TestSource 6 | import org.junit.platform.engine.support.descriptor.ClassSource 7 | import org.junit.platform.engine.support.descriptor.FilePosition 8 | import org.junit.platform.engine.support.descriptor.FileSource 9 | import org.junit.platform.engine.support.descriptor.MethodSource 10 | import java.io.File 11 | import java.lang.RuntimeException 12 | import java.net.URL 13 | import java.net.URLClassLoader 14 | 15 | private val slash: Char = File.separatorChar 16 | 17 | /** 18 | * Computes the pointer to the source of the test and returns it. Tries to compute at least inaccurate pointer. 19 | * @param testName the test name, used as a hint. null for `group{}`. 20 | * @return the pointer to the test source; returns null if the source can not be computed by any means. 21 | */ 22 | internal fun StackTraceElement.toTestSource(testName: String? = null): TestSource { 23 | val caller: StackTraceElement = this 24 | 25 | // Gradle is dumb to expect a hierarchy of ClassSources and MethodSources 26 | // Returning mixed FileSource will make Gradle freeze: https://github.com/gradle/gradle/issues/5737 27 | if (isRunningInsideGradle) { 28 | // return null // WARNING THIS WILL MAKE GRADLE SKIP TESTS!!!!! 29 | // throw RuntimeException("Unsupported") // THIS WILL MAKE GRADLE FREEZE!!! Retarded. 30 | // just returning ClassSource always will make gradle freeze. for fk's sake 31 | 32 | // strip $ to avoid having Test$1.xml, Test$1$5.xml with tests scattered in them 33 | var bareClassName: String = caller.className 34 | val bareClassNameOmittingInnerclass: String = bareClassName.replaceAfter('$', "").trim('$') 35 | if (ReflectionUtils.tryToLoadClass(bareClassNameOmittingInnerclass).toOptional().isPresent) { 36 | bareClassName = bareClassNameOmittingInnerclass 37 | val bareClassNameOmittingKtSuffix: String = bareClassName.removeSuffix("Kt") 38 | if (ReflectionUtils.tryToLoadClass(bareClassNameOmittingKtSuffix).toOptional().isPresent) { 39 | bareClassName = bareClassNameOmittingKtSuffix 40 | } 41 | } 42 | if (testName != null) { 43 | return MethodSource.from(bareClassName, testName) 44 | } 45 | 46 | // BEWARE: if the bareClassName doesn't exist, then JUnit 5 will skip this group and 47 | // all nested tests silently! 48 | // it's better to fail properly than to risk tests not being run. 49 | ReflectionUtils.tryToLoadClass(bareClassNameOmittingInnerclass).getOrThrow { RuntimeException("Failed to load class", it) } 50 | return ClassSource.from(bareClassName, caller.filePosition) 51 | } 52 | 53 | // normally we would just return ClassSource, but there are the following issues with that: 54 | // 1. Intellij ignores FilePosition in ClassSource; reported as https://youtrack.jetbrains.com/issue/IDEA-186581 55 | // 2. If I try to remedy that by passing in the block class name (such as DynaTestTest$1$1$1$1), Intellij looks confused and won't perform any navigation 56 | // 3. FileSource seems to work very well. 57 | 58 | // Try to guess the absolute test file name from the file class. It should be located somewhere in src/test/kotlin or src/test/java 59 | if (!caller.fileName.isNullOrBlank() && caller.fileName.endsWith(".kt") && caller.lineNumber > 0) { 60 | // workaround for https://youtrack.jetbrains.com/issue/IDEA-188466 61 | // the thing is that when using $MODULE_DIR$, IDEA will set CWD to, say, karibu-testing/.idea/modules/karibu-testing-v8 62 | // we need to revert that back to karibu-testing/karibu-testing-v8 63 | var moduleDir: File = File("").absoluteFile 64 | if (moduleDir.absolutePath.contains("$slash.idea${slash}modules")) { 65 | moduleDir = File(moduleDir.absolutePath.replace("$slash.idea${slash}modules", "")) 66 | } 67 | 68 | // discover the file 69 | val folders: List = listOf("java", "kotlin").map { File(moduleDir, "src/test/$it") }.filter { it.exists() } 70 | // don't use $slash here, since it's us who's producing those slashes 71 | val pkg: String = caller.className 72 | .replace('.', '/') 73 | .replaceAfterLast('/', "", "") 74 | .trim('/') 75 | var file: File? = folders.map { File(it, "$pkg/${caller.fileName}") }.firstOrNull { it.exists() } 76 | if (file == null) { 77 | // try another approach 78 | val clazz: Class<*>? = try { 79 | Class.forName(caller.className) 80 | } catch (e: ClassNotFoundException) { 81 | null 82 | } 83 | if (clazz != null && clazz != InternalTestingClass::class.java) { 84 | file = clazz.guessSourceFileName(caller.fileName) 85 | } 86 | } 87 | if (file != null) return FileSource.from(file, caller.filePosition) 88 | } 89 | 90 | // Intellij's ClassSource doesn't work on classes named DynaTestTest$1$1$1$1 (with $ in them); strip that. 91 | val bareClassName: String = caller.className.replaceAfter('$', "").trim('$') 92 | 93 | // We tried to resolve the test as FileSource, but we failed. Let's at least return the ClassSource. 94 | return ClassSource.from(bareClassName, caller.filePosition) 95 | } 96 | 97 | private val StackTraceElement.filePosition: FilePosition? get() = if (lineNumber > 0) FilePosition.from(lineNumber) else null 98 | 99 | /** 100 | * Guesses source file for given class. For Intellij it is able to discover sources also in another module, 101 | * for Gradle it only discovers sources in this module. 102 | * @param fileNameFromStackTraceElement the class is known to be present in this file. May be different than `simpleClassName.java` 103 | * in case of Kotlin where classes may reside in random files. 104 | */ 105 | internal fun Class<*>.guessSourceFileName(fileNameFromStackTraceElement: String): File? { 106 | val resource: String = `package`.name.replace('.', '/') + "/" + simpleName + ".class" 107 | val classLoader: ClassLoader = Thread.currentThread().contextClassLoader 108 | 109 | // in case of Intellij, the url is something like 110 | // file:/home/mavi/work/my/dynatest/dynatest-engine/out/production/classes/com/github/mvysny/dynatest/InternalTestingClassKt.class 111 | val url: URL = classLoader.getResource(resource) ?: return null 112 | 113 | // We have a File that points to a .class file. We need to resolve that to the source .java/.kt file. 114 | // The most valuable part of the path is the absolute project path in which the file may be present. 115 | // Then, the class may be located in some folder named `build/something` or `out/production/classes`. 116 | // We need to remove that part and replace it with the path to the file, e.g. `src/main/kotlin` 117 | 118 | // try to replace "out/production/classes" or "build/classes/java/test" with "src/main/kotlin" (or such) 119 | val fullPathToClassName: String = url.toFile()?.absolutePath?.replace('\\', '/') ?: return null 120 | val buildFolderRegex: Regex = "(build/classes/(java|kotlin)/[^/]+/)|out/production/classes/".toRegex() 121 | if (fullPathToClassName.contains(buildFolderRegex)) { 122 | for (srcPath in listOf("src/main/java/", "src/main/kotlin/", "src/test/java/", "src/test/kotlin/")) { 123 | val replacement: String = fullPathToClassName.replace(buildFolderRegex, srcPath) 124 | assert(replacement != fullPathToClassName) 125 | val potentialSourceFile = File(File(replacement).absoluteFile.parentFile, fileNameFromStackTraceElement) 126 | if (potentialSourceFile.exists()) { 127 | return potentialSourceFile 128 | } 129 | } 130 | } 131 | if (classLoader !is URLClassLoader) { 132 | // JDK9+ - can't do the classpath scanning, just bail out 133 | return null 134 | } 135 | 136 | // JDK8: scan the classpath and find the path that matches the file. 137 | val classpath: List = classLoader.urLs.toList() 138 | 139 | // classpath entry which contains given class 140 | val classpathEntry: URL = classpath.firstOrNull { url.toString().startsWith(it.toString()) } ?: return null 141 | val classOutputDir: File = classpathEntry.toFile() ?: return null 142 | 143 | // step out of classOutputDir, but only 4 folders tops, so that we don't end up searching user's filesystem 144 | // Intellij outputs to out/production/classes 145 | // Gradle outputs to build/classes/java/test 146 | 147 | // Doesn't work with Gradle! It will include dependent modules as jars, e.g. 148 | // file:/home/mavi/work/my/dynatest/dynatest-api/build/libs/dynatest-api-0.11-SNAPSHOT.jar 149 | // With Gradle, this will only resolve sources in current project. 150 | var potentialModuleDir: File = classOutputDir 151 | repeat(5) { 152 | 153 | val fileName: String = `package`.name.replace('.', '/') + "/" + fileNameFromStackTraceElement 154 | val potentialFiles = listOf("src/main/java", "src/main/kotlin", "src/test/java", "src/test/kotlin").map { 155 | "$potentialModuleDir/$it/$fileName" 156 | } 157 | 158 | val resolvedFileName: String? = potentialFiles.firstOrNull { File(it).exists() } 159 | 160 | if (resolvedFileName != null) { 161 | return File(resolvedFileName) 162 | } 163 | 164 | potentialModuleDir = potentialModuleDir.parentFile 165 | } 166 | // don't fail - if the user has the sources elsewhere, just bail out and return null 167 | // throw RuntimeException("Looking for $this - it was found in $url which is $potentialModuleDir but I can't find it in any of the source roots!") 168 | 169 | return null 170 | } 171 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /dynatest-engine/src/main/kotlin/com/github/mvysny/dynatest/engine/DynaTestEngine.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest.engine 2 | 3 | import com.github.mvysny.dynatest.DynaNodeGroup 4 | import com.github.mvysny.dynatest.DynaNodeTest 5 | import com.github.mvysny.dynatest.DynaTest 6 | import com.github.mvysny.dynatest.Outcome 7 | import org.junit.platform.commons.util.ReflectionUtils 8 | import org.junit.platform.engine.* 9 | import org.junit.platform.engine.discovery.* 10 | import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor 11 | import org.junit.platform.engine.support.descriptor.ClassSource 12 | import org.junit.platform.engine.support.hierarchical.EngineExecutionContext 13 | import org.junit.platform.engine.support.hierarchical.Node 14 | import java.lang.reflect.Modifier 15 | import java.util.* 16 | import java.util.function.Predicate 17 | 18 | /** 19 | * Since JUnit5's dynamic tests lack the necessary features, I'll implement my own Test Engine. In particular, JUnit5's dynamic tests: 20 | * * do not allow to reference the pointer to the source code of the test accurately: https://github.com/junit-team/junit5/issues/1293 21 | * * do not support beforeGroup/afterGroup: https://github.com/junit-team/junit5/issues/1292 22 | */ 23 | public class DynaTestEngine : TestEngine { 24 | 25 | private val classFilter: Predicate> = Predicate { it.isPublic && !it.isAbstract && DynaTest::class.java.isAssignableFrom(it) } 26 | 27 | override fun discover(request: EngineDiscoveryRequest, uniqueId: UniqueId): TestDescriptor { 28 | // this functionDynamicNode must never fail, otherwise JUnit5 will silently ignore this TestEngine and the user will never know. 29 | // that's why we will wrap any exception thrown by this method into a specialized, always failing TestDescriptor. 30 | // see https://github.com/gradle/gradle/issues/4418 for more details. 31 | 32 | fun buildClassNamePredicate(request: EngineDiscoveryRequest): Predicate { 33 | val filters = ArrayList>() 34 | filters.addAll(request.getFiltersByType(ClassNameFilter::class.java)) 35 | filters.addAll(request.getFiltersByType(PackageNameFilter::class.java)) 36 | return Filter.composeFilters(filters).toPredicate() 37 | } 38 | 39 | try { 40 | val classNamePredicate = buildClassNamePredicate(request) 41 | val classes = mutableSetOf>() 42 | 43 | request.getSelectorsByType(ClasspathRootSelector::class.java).forEach { selector -> 44 | ReflectionUtils.findAllClassesInClasspathRoot( 45 | selector.classpathRoot, classFilter, 46 | classNamePredicate 47 | ).forEach { classes.add(it) } 48 | } 49 | request.getSelectorsByType(PackageSelector::class.java).forEach { selector -> 50 | ReflectionUtils.findAllClassesInPackage(selector.packageName, classFilter, classNamePredicate) 51 | .forEach { classes.add(it) } 52 | } 53 | request.getSelectorsByType(ClassSelector::class.java).forEach { selector -> classes.add(selector.javaClass) } 54 | 55 | // todo filter based on UniqueIdSelector when https://youtrack.jetbrains.com/issue/IDEA-169198 is fixed 56 | 57 | val result = ClassListTestDescriptor(uniqueId) 58 | 59 | // filter out non-DynaTest classes as per https://github.com/gradle/gradle/issues/4418 60 | classes 61 | .filter { DynaTest::class.java.isAssignableFrom(it) } 62 | .forEach { 63 | try { 64 | // let's use newInstance() since the recommended way of getDeclaredConstructor().newInstance() 65 | // wraps any checked exceptions in InvocationTargetException which we really don't want in Kotlin :) 66 | @Suppress("DEPRECATION") 67 | val test: DynaTest = it.newInstance() as DynaTest 68 | val td = DynaNodeTestDescriptor(result.uniqueId, test.root) 69 | result.addChild(td) 70 | test.root.onDesignPhaseEnd() 71 | } catch (t: Throwable) { 72 | result.addChild(InitFailedTestDescriptor(result.uniqueId, it, t)) 73 | } 74 | } 75 | return result 76 | 77 | } catch (t: Throwable) { 78 | return InitFailedTestDescriptor(uniqueId, DynaTestEngine::class.java, t) 79 | } 80 | } 81 | 82 | override fun getId(): String = "DynaTest" 83 | 84 | override fun execute(request: ExecutionRequest) { 85 | 86 | fun TestDescriptor.engineExecutionListenerExecutionSuccess() { 87 | val skipped = (this as? DynaNodeTestDescriptor)?.isEnabled == false 88 | if (!skipped) { 89 | request.engineExecutionListener.executionFinished( 90 | this, 91 | TestExecutionResult.successful() 92 | ) 93 | } else { 94 | request.engineExecutionListener.executionSkipped( 95 | this, 96 | "x" 97 | ) 98 | } 99 | } 100 | 101 | /** 102 | * Runs all tests defined in this descriptor. This function does not throw exception if any of the 103 | * test/beforeEach/beforeAll/afterEach/afterAll fails. 104 | */ 105 | fun TestDescriptor.runAllTests() { 106 | // mark test started 107 | request.engineExecutionListener.executionStarted(this) 108 | 109 | // if this test descriptor denotes a DynaNodeGroup, run all `beforeGroup` blocks. 110 | try { 111 | (this as? DynaNodeTestDescriptor)?.runBeforeGroup() 112 | } catch (t: Throwable) { 113 | // one of the `beforeGroup` failed; do not run anything in this group (but still run all afterGroup blocks in this group) 114 | // mark the group as failed. 115 | (this as? DynaNodeTestDescriptor)?.runAfterGroup(t) 116 | request.engineExecutionListener.executionFinished(this, TestExecutionResult.failed(t)) 117 | // bail out, we're done. 118 | return 119 | } 120 | 121 | // beforeGroup ran successfully, continue with the normal test execution. 122 | children.forEach { childDescriptor -> childDescriptor.runAllTests() } 123 | 124 | try { 125 | if (this is DynaNodeTestDescriptor && this.isTest) { 126 | runTest() 127 | } else if (this is InitFailedTestDescriptor) { 128 | throw RuntimeException(failure) 129 | } 130 | (this as? DynaNodeTestDescriptor)?.runAfterGroup(null) 131 | engineExecutionListenerExecutionSuccess() 132 | } catch (t: Throwable) { 133 | request.engineExecutionListener.executionFinished(this, TestExecutionResult.failed(t)) 134 | } 135 | } 136 | 137 | request.rootTestDescriptor.runAllTests() 138 | } 139 | } 140 | 141 | /** 142 | * A container which hosts all DynaTest test classes wrapped in [DynaNodeTestDescriptor]s - they then in turn host individual groups and tests. 143 | * Returned by [DynaTestEngine.discover]. 144 | */ 145 | internal class ClassListTestDescriptor(uniqueId: UniqueId) : AbstractTestDescriptor(uniqueId, "DynaTest") { 146 | override fun getType(): TestDescriptor.Type = TestDescriptor.Type.CONTAINER 147 | } 148 | 149 | /** 150 | * Computes [UniqueId] for given [node], from the ID of the parent. 151 | * @receiver the parent ID. 152 | */ 153 | private fun UniqueId.append(node: DynaNodeImpl): UniqueId { 154 | val segmentType = when(node) { 155 | is DynaNodeTestImpl -> "test" 156 | is DynaNodeGroupImpl -> "group" 157 | } 158 | return append(segmentType, node.name) 159 | } 160 | 161 | internal class DynaNodeTestDescriptor(parentId: UniqueId, val node: DynaNodeImpl) : 162 | AbstractTestDescriptor(parentId.append(node), node.name, node.toTestSource()), Node { 163 | init { 164 | if (node is DynaNodeGroup) { 165 | // register children even if this group is disabled 166 | // this will cause Intellij to display those tests 167 | (node as DynaNodeGroupImpl).children.forEach { addChild(DynaNodeTestDescriptor(uniqueId, it)) } 168 | } 169 | } 170 | 171 | val isEnabled: Boolean get() = node.enabled 172 | 173 | override fun getType(): TestDescriptor.Type = when (node) { 174 | is DynaNodeGroupImpl -> TestDescriptor.Type.CONTAINER 175 | is DynaNodeTestImpl -> TestDescriptor.Type.TEST 176 | } 177 | 178 | fun runBeforeGroup() { 179 | if (node is DynaNodeGroup && isEnabled) { 180 | (node as DynaNodeGroupImpl).beforeGroup.forEach { it(Unit) } 181 | } 182 | } 183 | 184 | /** 185 | * Runs all [DynaNodeGroupImpl.afterGroup] on [node], even if some if the blocks fails with an exception. 186 | * @param t if not null, the test has failed with this exception. 187 | * @throws Throwable if any of the block fails or [t] was not null. 188 | */ 189 | fun runAfterGroup(t: Throwable?) { 190 | var tf = t 191 | if (node is DynaNodeGroup && isEnabled) { 192 | (node as DynaNodeGroupImpl).afterGroup.forEach { 193 | try { 194 | it(Unit, Outcome(null, tf)) 195 | } catch (ex: Throwable) { 196 | if (tf == null) tf = ex else tf!!.addSuppressed(ex) 197 | } 198 | } 199 | } 200 | if (tf != null && t == null) throw tf!! 201 | } 202 | 203 | /** 204 | * Runs given [block], properly prefixed with calls to `beforeEach` blocks and postfixed with calls to `afterEach` blocks. 205 | * If any of those fails, does a proper cleanup and then throws the exception. 206 | */ 207 | private fun runBlock(testName: String, block: () -> Unit) { 208 | var lastNodeWithBeforeEachRan: DynaNodeTestDescriptor? = null 209 | try { 210 | getPathFromRoot().forEach { descriptor -> 211 | lastNodeWithBeforeEachRan = descriptor 212 | if (descriptor.node is DynaNodeGroup) { 213 | (descriptor.node as DynaNodeGroupImpl).beforeEach.forEach { it(Unit) } 214 | } 215 | } 216 | block() 217 | } catch(t: Throwable) { 218 | lastNodeWithBeforeEachRan?.runAfterEach(testName, t) 219 | throw t 220 | } 221 | lastNodeWithBeforeEachRan?.runAfterEach(testName, null) 222 | } 223 | 224 | fun runTest() { 225 | if (isEnabled) { 226 | runBlock(node.name) { (node as DynaNodeTestImpl).body(node) } 227 | } 228 | } 229 | 230 | /** 231 | * Computes the path of dyna nodes from the root group towards this one. 232 | */ 233 | private fun getPathFromRoot(): List = 234 | generateSequence(this, { it: DynaNodeTestDescriptor -> it.parent.orElse(null) as? DynaNodeTestDescriptor }) 235 | .toList() 236 | .reversed() 237 | 238 | /** 239 | * Runs all `afterEach` blocks recursively, from this node all the way up to the root node. Properly propagates exceptions. 240 | * @param testFailure if not null, the test has failed with this exception. 241 | */ 242 | private fun runAfterEach(testName: String, testFailure: Throwable?) { 243 | var tf: Throwable? = testFailure 244 | if (isEnabled) { 245 | if (node is DynaNodeGroup) { 246 | (node as DynaNodeGroupImpl).afterEach.forEach { afterEachBlock: Unit.(Outcome) -> Unit -> 247 | try { 248 | afterEachBlock(Unit, Outcome(testName, tf)) 249 | } catch (t: Throwable) { 250 | if (tf == null) tf = t else tf!!.addSuppressed(t) 251 | } 252 | } 253 | } 254 | (parent.orElse(null) as? DynaNodeTestDescriptor)?.runAfterEach( 255 | testName, 256 | tf 257 | ) 258 | } 259 | if (testFailure == null && tf != null) throw tf!! 260 | } 261 | 262 | override fun shouldBeSkipped(context: EngineExecutionContext): Node.SkipResult = when { 263 | node.enabled -> Node.SkipResult.doNotSkip() 264 | else -> Node.SkipResult.skip("x") 265 | } 266 | } 267 | 268 | /** 269 | * Returns true if this class is private. 270 | */ 271 | public val Class<*>.isPrivate: Boolean get() = Modifier.isPrivate(modifiers) 272 | /** 273 | * Returns true if this class is abstract. 274 | */ 275 | public val Class<*>.isAbstract: Boolean get() = Modifier.isAbstract(modifiers) 276 | /** 277 | * Returns true if this class is public. 278 | */ 279 | public val Class<*>.isPublic: Boolean get() = Modifier.isPublic(modifiers) 280 | 281 | /** 282 | * When the [DynaTest]'s block fails to run properly and produce tests, [DynaTestEngine.discover] will return this test descriptor to mark 283 | * the whole DynaTest as failed. Even more, the whole [DynaTestEngine.discover] method is wrapped in try-catch which will produce this test 284 | * descriptor on failure. This way, the [DynaTestEngine.discover] method never fails (which is very important: see https://github.com/gradle/gradle/issues/4418 285 | * for more details). 286 | */ 287 | internal class InitFailedTestDescriptor(parentId: UniqueId, clazz: Class<*>, val failure: Throwable) : 288 | AbstractTestDescriptor(parentId.append("class", clazz.simpleName), clazz.simpleName, ClassSource.from(clazz)) { 289 | 290 | override fun getType(): TestDescriptor.Type = TestDescriptor.Type.TEST 291 | } 292 | -------------------------------------------------------------------------------- /dynatest-engine/src/test/kotlin/com/github/mvysny/dynatest/DynaTestEngineTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.mvysny.dynatest 2 | 3 | import java.io.IOException 4 | import java.lang.IllegalArgumentException 5 | import kotlin.test.expect 6 | import kotlin.test.fail 7 | 8 | /** 9 | * Bunch of tests for the engine. 10 | * 11 | * If you add tests here, don't forget to fix the `build.gradle.kts` - there is a check that there are in fact 33 12 | * test methods executed by this test. 13 | */ 14 | class DynaTestEngineTest : DynaTest({ 15 | group("test the 'beforeEach' behavior") { 16 | 17 | test("test that beforeEach runs before every test") { 18 | runTests { 19 | var called = false 20 | test("check that 'beforeEach' ran") { 21 | expect(true) { called } 22 | } 23 | beforeEach { called = true } 24 | } 25 | } 26 | 27 | test("test that 'beforeEach' is also applied to tests nested inside a child group") { 28 | runTests { 29 | var called = false 30 | // an artificial group, only for the purpose of nesting the test that checks whether the 'beforeEach' block ran 31 | group("artificial group") { 32 | test("check that 'beforeEach' ran") { 33 | expect(true) { called } 34 | } 35 | } 36 | beforeEach { called = true } 37 | } 38 | } 39 | 40 | test("when beforeEach throws, the test is not called") { 41 | expectFailures({ 42 | runTests { 43 | beforeEach { throw RuntimeException("expected") } 44 | test("should not have been called") { fail("should not have been called since beforeEach failed") } 45 | } 46 | }) { 47 | expectStats(0, 1, 0) 48 | expectFailure("should not have been called") 49 | } 50 | } 51 | 52 | test("when beforeEach throws, the afterEach is still called") { 53 | var called = false 54 | expectFailures({ 55 | runTests { 56 | beforeEach { throw RuntimeException("expected") } 57 | test("should not have been called") { fail("should not have been called since beforeEach failed") } 58 | afterEach { called = true } 59 | } 60 | }) { 61 | expectStats(0, 1, 0) 62 | expectFailure("should not have been called") 63 | } 64 | expect(true) { called } 65 | } 66 | } 67 | 68 | group("test the 'afterEach' behavior") { 69 | test("test that 'afterEach' runs after every test") { 70 | lateinit var outcome: Outcome 71 | runTests { 72 | afterEach { outcome = it } 73 | test("dummy test which triggers 'afterEach'") {} 74 | } 75 | expect(true) { outcome.isSuccess } 76 | } 77 | 78 | test("test that 'afterEach' is also applied to tests nested inside a child group") { 79 | var called = 0 80 | runTests { 81 | afterEach { called++ } 82 | 83 | // an artificial group, only for the purpose of nesting the test that checks whether the 'afterEach' block ran 84 | group("artificial group") { 85 | test("dummy test which triggers 'afterEach'") {} 86 | } 87 | } 88 | expect(1) { called } 89 | } 90 | 91 | test("when both beforeEach and afterEach throws, the afterEach's exception is added as suppressed") { 92 | var called = false 93 | expectFailures({ 94 | runTests { 95 | beforeEach { throw RuntimeException("expected") } 96 | test("should not have been called") { called = true; fail("should not have been called since beforeEach failed") } 97 | afterEach { throw IOException("simulated") } 98 | } 99 | }) { 100 | expectStats(0, 1, 0) 101 | expect>(IOException::class.java) { getFailure("should not have been called").suppressed[0].javaClass } 102 | } 103 | expect(false) { called } 104 | } 105 | 106 | test("throwing in `afterEach` will make the test fail") { 107 | expectFailures({ 108 | runTests { 109 | test("dummy") {} 110 | afterEach { throw IOException("simulated") } 111 | } 112 | }) { 113 | expectStats(0, 1, 0) 114 | expectFailure("dummy") 115 | } 116 | } 117 | 118 | test("throwing in test should invoke all `afterEach`") { 119 | expectFailures({ 120 | runTests { 121 | test("simulated failure") { throw RuntimeException("simulated") } 122 | afterEach { throw IOException("simulated") } 123 | } 124 | }) { 125 | expectStats(0, 1, 0) 126 | expect>(IOException::class.java) { getFailure("simulated failure").suppressed[0].javaClass } 127 | } 128 | } 129 | 130 | test("all `afterEach` should have been invoked even if some of them fail") { 131 | lateinit var outcome: Outcome 132 | expectFailures({ 133 | runTests { 134 | test("dummy") {} 135 | afterEach { throw RuntimeException("simulated") } 136 | afterEach { outcome = it } 137 | } 138 | }) { 139 | expectStats(0, 1, 0) 140 | expectFailure("dummy") 141 | } 142 | expect(true) { outcome.isFailure } 143 | expect?>(RuntimeException::class.java) { outcome.failureCause?.javaClass } 144 | expect("simulated") { outcome.failureCause?.message } 145 | } 146 | 147 | test("if `beforeEach` fails, no `afterEach` in subgroup should be called") { 148 | var called = false 149 | expectFailures({ 150 | runTests { 151 | beforeEach { throw RuntimeException("simulated") } 152 | group("nested group") { 153 | test("dummy") { called = true; fail("should not have been called") } 154 | afterEach { called = true; fail("should not have been called") } 155 | } 156 | } 157 | }) { 158 | expectStats(0, 1, 0) 159 | expectFailure("dummy") 160 | expect(listOf()) { getFailure("dummy").suppressed.toList() } 161 | } 162 | expect(false) { called } 163 | } 164 | } 165 | 166 | group("test the 'beforeGroup' behavior") { 167 | test("simple before-test") { 168 | var called = false 169 | runTests { 170 | test("check that 'beforeGroup' ran") { 171 | expect(true) { called } 172 | } 173 | beforeGroup { called = true } 174 | } 175 | expect(true) { called } 176 | } 177 | 178 | test("before-group") { 179 | var called = false 180 | runTests { 181 | group("artificial group") { 182 | test("check that 'beforeEach' ran") { 183 | expect(true) { called } 184 | } 185 | } 186 | beforeGroup { called = true } 187 | } 188 | expect(true) { called } 189 | } 190 | 191 | group("test when `beforeGroup` fails") { 192 | test("`beforeEach`, `test`, `afterEach`, `afterGroup` doesn't get called when `beforeGroup` fails") { 193 | var called = false 194 | expectFailures({ 195 | runTests { 196 | beforeGroup { throw RuntimeException("Simulated") } 197 | beforeEach { called = true; fail("shouldn't be called") } 198 | test("shouldn't be called") { called = true; fail("shouldn't be called") } 199 | afterEach { called = true; fail("shouldn't be called") } 200 | } 201 | }) { 202 | expectStats(0, 1, 0) 203 | expectFailure("root") 204 | expect(0) { getFailure("root").suppressed.size } 205 | } 206 | expect(false) { called } 207 | } 208 | } 209 | } 210 | 211 | group("test the 'afterGroup' behavior") { 212 | test("simple after-test") { 213 | var called = 0 214 | runTests { 215 | afterGroup { called++ } 216 | test("dummy test") {} 217 | test("dummy test2") {} 218 | } 219 | expect(1) { called } 220 | } 221 | 222 | test("`afterGroup` is called only once even when there are nested groups") { 223 | var called = 0 224 | runTests { 225 | afterGroup { called++ } 226 | 227 | group("artificial group") { 228 | test("dummy test which triggers 'afterEach'") {} 229 | } 230 | } 231 | expect(1) { called } 232 | } 233 | 234 | group("exceptions") { 235 | test("`afterGroup` is called even if `beforeGroup` fails") { 236 | var called = 0 237 | expectFailures({ 238 | runTests { 239 | beforeGroup { throw RuntimeException("Simulated") } 240 | afterGroup { called++ } 241 | } 242 | }) { 243 | expectStats(0, 1, 0) 244 | } 245 | expect(1) { called } 246 | } 247 | 248 | test("Exceptions thrown in `afterGroup` are attached as suppressed to the exception thrown in `beforeGroup`") { 249 | expectFailures({ 250 | runTests { 251 | beforeGroup { throw RuntimeException("Simulated") } 252 | afterGroup { throw IOException("Simulated") } 253 | } 254 | }) { 255 | expectStats(0, 1, 0) 256 | expectFailure("root") 257 | expect>(IOException::class.java) { getFailure("root").suppressed[0].javaClass } 258 | } 259 | } 260 | 261 | test("Exceptions thrown from `beforeGroup` do not prevent other groups from running") { 262 | var called = false 263 | expectFailures({ 264 | runTests { 265 | group("failing group") { 266 | beforeGroup { throw RuntimeException("Simulated") } 267 | } 268 | group("successful group") { 269 | test("test") { called = true } 270 | } 271 | } 272 | }) { 273 | expectStats(1, 1, 0) 274 | expectFailure("failing group") 275 | } 276 | expect(true) { called } 277 | } 278 | 279 | test("Failure in `afterGroup` won't prevent `afterGroup` from being called in the parent group") { 280 | var called = false 281 | expectFailures({ 282 | runTests { 283 | group("Failing group") { 284 | test("dummy") {} 285 | afterGroup { throw RuntimeException("Simulated") } 286 | } 287 | afterGroup { called = true } 288 | } 289 | }) { 290 | expectStats(1, 1, 0) 291 | } 292 | expect(true) { called } 293 | } 294 | 295 | test("Failure in a test won't prevent `afterGroup` from being called") { 296 | var called = false 297 | expectFailures({ 298 | runTests { 299 | test("failing") { fail("simulated") } 300 | afterGroup { called = true } 301 | } 302 | }) { 303 | expectStats(0, 1, 0) 304 | } 305 | expect(true) { called } 306 | } 307 | 308 | test("Failure in a `afterEach` won't prevent `afterGroup` from being called") { 309 | var called = false 310 | expectFailures({ 311 | runTests { 312 | test("dummy") {} 313 | afterEach { throw RuntimeException("Simulated") } 314 | afterGroup { called = true } 315 | } 316 | }) { 317 | expectStats(0, 1, 0) 318 | } 319 | expect(true) { called } 320 | } 321 | } 322 | } 323 | 324 | group("nesting DynaTest inside a test block is forbidden") { 325 | test("calling test") { 326 | var called = false 327 | expectFailures({ 328 | runTests { 329 | test("should fail") { 330 | this@runTests.test("can't define a test inside a test") { called = true } 331 | } 332 | } 333 | }) { 334 | expectStats(0, 1, 0) 335 | expectFailure("should fail") 336 | } 337 | expect(false) { called } 338 | } 339 | 340 | test("calling beforeGroup") { 341 | var called = false 342 | expectFailures({ 343 | runTests { 344 | test("should fail") { 345 | this@runTests.beforeGroup { called = true } 346 | } 347 | } 348 | }) { 349 | expectStats(0, 1, 0) 350 | expectFailure("should fail") 351 | } 352 | expect(false) { called } 353 | } 354 | 355 | test("calling beforeEach") { 356 | var called = false 357 | expectFailures({ 358 | runTests { 359 | test("should fail") { 360 | this@runTests.beforeEach { called = true } 361 | } 362 | } 363 | }) { 364 | expectStats(0, 1, 0) 365 | expectFailure("should fail") 366 | } 367 | expect(false) { called } 368 | } 369 | 370 | test("calling afterEach") { 371 | var called = false 372 | expectFailures({ 373 | runTests { 374 | test("should fail") { 375 | this@runTests.afterEach { called = true } 376 | } 377 | } 378 | }) { 379 | expectStats(0, 1, 0) 380 | expectFailure("should fail") 381 | } 382 | expect(false) { called } 383 | } 384 | 385 | test("calling afterGroup") { 386 | var called = false 387 | expectFailures({ 388 | runTests { 389 | test("should fail") { 390 | this@runTests.afterGroup { called = true } 391 | } 392 | } 393 | }) { 394 | expectStats(0, 1, 0) 395 | expectFailure("should fail") 396 | } 397 | expect(false) { called } 398 | } 399 | } 400 | 401 | group("make sure Gradle runs same-named tests") { 402 | // this is actually checked by the build.gradle.kts 403 | group("group 1") { 404 | test("a test") { "foo".indices.count { "foo".substring(it).startsWith("bar") } } 405 | } 406 | group("group 2") { 407 | test("a test") {} 408 | } 409 | } 410 | 411 | // intellij gets confused and only runs the first test; even if the second one fails, the failure is not reported 412 | // in the UI. 413 | group("prohibit same-named tests/groups in the same group") { 414 | test("test/test") { 415 | var called = 0 416 | expectThrows("test/group with name 'duplicite' is already present: duplicite") { 417 | runTests { 418 | test("duplicite") { called++; fail("shouldn't be called") } 419 | test("duplicite") { called++; fail("shouldn't be called") } 420 | } 421 | } 422 | expect(0) { called } 423 | } 424 | test("test/group") { 425 | var called = 0 426 | expectThrows("test/group with name 'duplicite' is already present: duplicite") { 427 | runTests { 428 | test("duplicite") { called++; fail("shouldn't be called") } 429 | group("duplicite") { called++; fail("shouldn't be called") } 430 | } 431 | } 432 | expect(0) { called } 433 | } 434 | test("group/test") { 435 | var called = 0 436 | expectThrows("test/group with name 'duplicite' is already present: duplicite") { 437 | runTests { 438 | group("duplicite") { } 439 | test("duplicite") { called++; fail("shouldn't be called") } 440 | } 441 | } 442 | expect(0) { called } 443 | } 444 | test("group/group") { 445 | var called = 0 446 | expectThrows(IllegalArgumentException::class, "test/group with name 'duplicite' is already present: duplicite") { 447 | runTests { 448 | group("duplicite") { } 449 | group("duplicite") { called++; fail("shouldn't be called") } 450 | } 451 | } 452 | expect(0) { called } 453 | } 454 | } 455 | 456 | group("xtest") { 457 | test("xtest body not called") { 458 | var called = 0 459 | runTests { 460 | xtest("xtest") { called++ } 461 | } 462 | expect(0) { called } 463 | } 464 | test("beforeEach/afterEach not called") { 465 | var called = 0 466 | runTests { 467 | beforeEach { called++ } 468 | afterEach { called++ } 469 | xtest("test") { called++ } 470 | } 471 | expect(0) { called } 472 | } 473 | test("parent beforeEach/afterEach not called") { 474 | var called = 0 475 | runTests { 476 | beforeEach { called++ } 477 | afterEach { called++ } 478 | group("foo") { 479 | xtest("test") { called++ } 480 | } 481 | } 482 | expect(0) { called } 483 | } 484 | test("beforeGroup/afterGroup still called since the group is enabled") { 485 | var called = 0 486 | runTests { 487 | beforeGroup { called++ } 488 | afterGroup { called++ } 489 | xtest("test") { called++ } 490 | } 491 | expect(2) { called } 492 | } 493 | test("parent beforeGroup/afterGroup still called since the group is enabled") { 494 | var called = 0 495 | runTests { 496 | beforeGroup { called++ } 497 | afterGroup { called++ } 498 | group("foo") { 499 | xtest("test") { called++ } 500 | } 501 | } 502 | expect(2) { called } 503 | } 504 | xtest("this test must be marked in intellij as skipped") { 505 | fail("should be skipped") 506 | } 507 | } 508 | group("xgroup") { 509 | test("tests not called") { 510 | var called = 0 511 | runTests { 512 | xgroup("xgroup") { 513 | test("should be skipped") { called++ } 514 | } 515 | } 516 | expect(0) { called } 517 | } 518 | test("beforeEach/afterEach not called") { 519 | var called = 0 520 | runTests { 521 | xgroup("disabled") { 522 | beforeEach { called++; fail("x") } 523 | afterEach { called++; fail("x") } 524 | test("test") { called++; fail("x") } 525 | } 526 | } 527 | expect(0) { called } 528 | } 529 | test("parent beforeEach/afterEach not called") { 530 | var called = 0 531 | runTests { 532 | beforeEach { called++; fail("x") } 533 | afterEach { called++; fail("x") } 534 | xgroup("disabled") { 535 | test("test") { called++; fail("x") } 536 | } 537 | } 538 | expect(0) { called } 539 | } 540 | test("beforeGroup/afterGroup still called since the group is enabled") { 541 | var called = 0 542 | runTests { 543 | beforeGroup { called++ } 544 | afterGroup { called++ } 545 | xgroup("disabled") { 546 | test("test") { called++ } 547 | } 548 | } 549 | expect(2) { called } 550 | } 551 | test("beforeGroup/afterGroup not called for disabled group") { 552 | var called = 0 553 | runTests { 554 | xgroup("disabled") { 555 | beforeGroup { called++ } 556 | afterGroup { called++ } 557 | test("test") { called++ } 558 | } 559 | } 560 | expect(0) { called } 561 | } 562 | xgroup("group must be marked in intellij as skipped") { 563 | test("test must be marked in intellij as skipped") { 564 | fail("should be skipped") 565 | } 566 | } 567 | } 568 | }) 569 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DynaTest Dynamic Testing 2 | 3 | [![GitHub tag](https://img.shields.io/github/tag/mvysny/dynatest.svg)](https://github.com/mvysny/dynatest/tags) 4 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.mvysny.dynatest/dynatest-engine/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.mvysny.dynatest/dynatest-engine) 5 | [![Build Status](https://github.com/mvysny/dynatest/actions/workflows/gradle.yml/badge.svg)](https://github.com/mvysny/dynatest/actions/workflows/gradle.yml) 6 | 7 | ## DEPRECATED 8 | 9 | This framework is DEPRECATED, will no longer be maintained. Leaving the source code on GitHub as a reference. 10 | See at the bottom of this file for reasons to deprecate, and for migration tips. 11 | 12 | The original README follows. 13 | 14 | ## DynaTest 15 | 16 | The simplest and most powerful testing framework for Kotlin. 17 | 18 | We promote builders over annotations. Instead of having annotations forming an unfamiliar 19 | [embedded mini-language, interpreted by magic at runtime](https://blog.softwaremill.com/the-case-against-annotations-4b2fb170ed67), 20 | we let you create your test methods using Kotlin - an actual programming language familiar to you. 21 | 22 | We don't program in annotations, after all. We program in Kotlin. 23 | 24 | Java compatibility matrix: 25 | * DynaTest 0.19 and lower require Java 7+ 26 | * DynaTest 0.20+ require Java 8+ 27 | 28 | ## Example Code 29 | 30 | Code example of the [CalculatorTest.kt](dynatest-engine/src/test/kotlin/com/github/mvysny/dynatest/CalculatorTest.kt): 31 | 32 | ```kotlin 33 | class Calculator { 34 | fun plusOne(i: Int) = i + 1 35 | fun close() {} 36 | } 37 | 38 | /** 39 | * A test case. 40 | */ 41 | class CalculatorTest : DynaTest({ 42 | 43 | /** 44 | * Top-level test. 45 | */ 46 | test("calculator instantiation test") { 47 | Calculator() 48 | } 49 | 50 | // you can have as many groups as you like, and you can nest them 51 | // 'group' has no semantic definition, you are free to assign one as you need 52 | group("tests the plusOne() function") { 53 | 54 | // demo of the very simple test 55 | test("one plusOne") { 56 | expect(2) { Calculator().plusOne(1) } 57 | } 58 | 59 | // nested group 60 | group("positive numbers") { 61 | // you can even create a reusable test battery, call it from anywhere and use any parameters you like. 62 | calculatorBattery(0..10) 63 | calculatorBattery(100..110) 64 | } 65 | 66 | group("negative numbers") { 67 | calculatorBattery(-50..-40) 68 | } 69 | } 70 | }) 71 | 72 | /** 73 | * Demonstrates a reusable test battery which can be called repeatedly and parametrized. 74 | * @receiver all tests+groups do not run immediately, but instead they register themselves to this group; they are run later on 75 | * when launched by JUnit5 76 | * @param range parametrized battery demo 77 | */ 78 | @DynaTestDsl 79 | fun DynaNodeGroup.calculatorBattery(range: IntRange) { 80 | require(!range.isEmpty()) 81 | 82 | group("plusOne on $range") { 83 | lateinit var c: Calculator 84 | 85 | // analogous to @Before in JUnit4, or @BeforeEach in JUnit5 86 | beforeEach { c = Calculator() } 87 | // analogous to @After in JUnit4, or @AfterEach in JUnit5 88 | afterEach { c.close() } 89 | 90 | // we can even generate test cases in a loop 91 | for (i in range) { 92 | test("plusOne($i) == ${i + 1}") { 93 | expect(i + 1) { c.plusOne(i) } 94 | } 95 | } 96 | } 97 | } 98 | ``` 99 | 100 | Running this in your IDE will produce: 101 | 102 | ![DynaTest CalculatorTest screenshot](images/dynatest.png) 103 | 104 | ## Tips & Tricks 105 | 106 | Intellij by default runs tests using Gradle. Dynatest contains workaround around 107 | Gradle and Intellij bugs, disabling navigation to the test source code from the 108 | *Run Tests* Intellij window. To fix this issue, head to *File* / *Settings* / 109 | *Build, Execution, Deployment* / *Build Tools* / *Gradle* and make sure that 110 | the *Run tests using* option is set to Intellij. 111 | 112 | ## Using DynaTest In Your Projects 113 | 114 | ### Example Project 115 | 116 | Please see [karibu-helloworld-application](https://github.com/mvysny/karibu-helloworld-application) 117 | for a very simple Gradle-based project employing DynaTest, to see how to integrate DynaTest 118 | with your app. 119 | 120 | ### Gradle+DynaTest Integration Guide 121 | 122 | DynaTest runs on top of JUnit5 engine, but it ignores any JUnit5 tests and only runs `DynaTest` tests. As a first step, 123 | add the test dependency on this library to your `build.gradle` file: 124 | 125 | ```groovy 126 | repositories { 127 | mavenCentral() 128 | } 129 | dependencies { 130 | testCompile("com.github.mvysny.dynatest:dynatest:x.y") 131 | } 132 | ``` 133 | 134 | > Note: check the tag number on the top for the newest version 135 | 136 | DynaTest will transitively include JUnit5's core. 137 | 138 | If you are using older Gradle 4.5.x or earlier which does not have native support for JUnit5, to actually 139 | run the tests you will need to add the [junit5-gradle-consumer](https://github.com/junit-team/junit5-samples/tree/r5.0.3/junit5-gradle-consumer) 140 | plugin to your build script. Please see the plugin's documentation for details. 141 | 142 | If you are using Gradle 4.6 or later, JUnit5 support is built-in; all you need to enable it is to insert this into your `build.gradle` file: 143 | 144 | ```groovy 145 | test { 146 | useJUnitPlatform() 147 | } 148 | ``` 149 | or `build.gradle.kts`: 150 | ```kotlin 151 | tasks.withType { 152 | useJUnitPlatform() 153 | } 154 | ``` 155 | 156 | (more on Gradle's JUnit5 support here: [Gradle JUnit5 support](https://docs.gradle.org/4.6-rc-1/release-notes.html#junit-5-support)) 157 | 158 | If you want to run JUnit5 tests along the DynaTest tests as well, you can run both DynaTest test engine along with JUnit5 Jupiter engine 159 | (which will only run JUnit5 tests and will ignore DynaTest tests): 160 | 161 | ```groovy 162 | dependencies { 163 | testRuntime("org.junit.jupiter:junit-jupiter-engine:5.8.2") 164 | } 165 | ``` 166 | 167 | ## DynaTest Guide 168 | 169 | DynaTest is composed of just 6 methods (and 0 annotations). 170 | 171 | Calling the `test("name") { test body }` function creates a new test and schedules it to be run by JUnit5 core. Example: 172 | ```kotlin 173 | class MyTest : DynaTest({ 174 | test("'save' button saves data") { 175 | button.click() 176 | expect(1) { Person.findAll().size } 177 | } 178 | }) 179 | ``` 180 | 181 | Calling the `group("name") { register more groups and tests }` function creates a test group and allows you to define tests (or 182 | more groups) inside of it. By itself the group does nothing more than nesting tests in your IDE output when you run 183 | tests; however it becomes very powerful with the lifecycle methods `beforeGroup`/`afterGroup`. Example: 184 | ```kotlin 185 | class MyTest : DynaTest({ 186 | group("String.length tests") { 187 | test("Empty string has zero length") { 188 | expect(0) { "".length } 189 | } 190 | } 191 | }) 192 | ``` 193 | 194 | > Technical detail: You write your test suite by extending the `DynaTest` class. The DynaTest constructor runs a block 195 | which allows you to register tests and groups. 196 | The block is pure Kotlin so you can use for loops, reusable functions and all features of the Kotlin programming language. 197 | 198 | ### Test Lifecycle Listeners 199 | 200 | Often you need to prepare some kind of environment before a test, and tear it down afterwards. For that simply call 201 | the following functions: 202 | 203 | `beforeEach { body }` schedules given block to run before every individual test, both in this group and in all subgroups. If the body fails, 204 | the test is not run and is marked failed. Example: 205 | ```kotlin 206 | class CalculatorTest : DynaTest({ 207 | lateinit var calculator: Calculator 208 | beforeEach { calculator = Calculator() } 209 | afterEach { calculator.close() } 210 | 211 | test("0+1=1") { 212 | expect(1) { calculator.plusOne(0) } 213 | } 214 | }) 215 | ``` 216 | 217 | `afterEach { body }` schedules given block to run after every individual test, both in this group and in all subgroups. 218 | The body is ran even if the test itself or the `beforeEach` block failed. See `beforeEach` for an example. 219 | 220 | `beforeGroup { body }` schedules given block to run once before every test in this group. The block won't run again for subgroups. 221 | If the block fails, no tests/beforeEach/afterEach from this group and its subgroups are executed and they will be all marked as failed. This is a 222 | direct replacement for `@BeforeClass` in JUnit, but it is a lot more powerful since you can use it on subgroups as well. You 223 | typically use `beforeGroup` to start something that is expensive to construct/start, e.g. a Jetty server: 224 | ```kotlin 225 | class ServerTest : DynaTest({ 226 | lateinit var server: Server 227 | beforeGroup { server = Server(8080); server.start() } 228 | afterGroup { server.stop() } 229 | 230 | test("ping") { 231 | expect("OK") { URL("http://localhost:8080/status").readText() } 232 | } 233 | }) 234 | ``` 235 | 236 | `afterGroup { body }` schedules given block to run after the group concluded running its tests, both in this group and in all subgroups. 237 | The body is ran even if the test itself, or any `beforeEach`/`afterEach` or even `beforeGroup` blocks failed. 238 | See `beforeGroup` for an example. 239 | 240 | ### Conclusion 241 | 242 | Now you have a good understanding of all the machinery DynaTest has to offer. This is completely enough for simple tests. 243 | Now we move to advanced topics on how to put this machinery to good use for more advanced scenarios. 244 | 245 | ## File/Directory Utilities 246 | 247 | dynatest 0.18 and later contains support for filesystem-related assertions and ops: 248 | 249 | * `File.expectExists()` 250 | * `File.expectNotExists()` 251 | * `File.expectFile()` 252 | * `File.expectDirectory()` 253 | * `File.expectReadableFile()` 254 | * `File.expectWritableFile()` 255 | 256 | You can use Kotlin built-in `createTempDir()` and `createTempFile()` global functions to create temporary 257 | folders and files; use Kotlin built-in `copyRecursively()` to copy entire folders. 258 | 259 | If you need to assert that given folder contains certain amount of files, use the 260 | `File.expectFiles()` function as follows: 261 | 262 | * `File("build").expectFiles("generated/**/*.java", 40..50)` 263 | 264 | ### Temporary Directories 265 | 266 | You can use the `withTempDir()` helper function to create a test folder before every test, 267 | then tear it down afterwards: 268 | 269 | ```kotlin 270 | group("source generator tests") { 271 | val sources: File by withTempDir("sources") 272 | test("simple") { 273 | generateSourcesTo(sources) 274 | val generatedFiles: List = sources.expectFiles("*.java", 10..10) 275 | // ... 276 | } 277 | test("more complex test") { 278 | // 'sources' will point to a new temporary directory now. 279 | generateSourcesTo(sources) 280 | // ... 281 | } 282 | } 283 | ``` 284 | 285 | To create a reusable utility function which e.g. pre-populates the directory, you have 286 | to use a different syntax: 287 | 288 | ```kotlin 289 | @DynaTestDsl 290 | fun DynaNodeGroup.withSources(): ReadWriteProperty { 291 | val sourcesProperty: ReadWriteProperty = withTempDir("sources") 292 | val sources by sourcesProperty 293 | beforeEach { 294 | generateSourcesTo(sources) 295 | } 296 | return sourcesProperty 297 | } 298 | 299 | group("source generator tests") { 300 | val sources: File by withSources() 301 | test("simple") { 302 | sources.expectFiles("*.java", 10..10) 303 | } 304 | } 305 | ``` 306 | 307 | Make sure to never return `sources` since that would query the value of the `sourcesProperty` 308 | right away, failing with `unitialized` `RuntimeException`. 309 | 310 | Alternatively, since DynaTest 0.22 you can take advantage of `withTempDir()`'s init block: 311 | 312 | ```kotlin 313 | @DynaTestDsl 314 | fun DynaNodeGroup.withSources(): ReadWriteProperty = 315 | withTempDir("sources") { dir -> generateSourcesTo(dir) } 316 | 317 | group("source generator tests") { 318 | val sources: File by withSources() 319 | test("simple") { 320 | sources.expectFiles("*.java", 10..10) 321 | } 322 | } 323 | ``` 324 | 325 | ### Lazy-init variables in general 326 | 327 | The above examples served only for a specific case of having a temporary dir accessible 328 | to a bunch of tests. However, you can take advantage of the `LateinitProperty` class 329 | to create any kind of variable. For example, say that we have a `TestProject` 330 | class which internally creates a temp folder and sets up some kind of a test project, then deletes it afterwards: 331 | 332 | ```kotlin 333 | @DynaTestDsl 334 | fun DynaNodeGroup.withTestProject(): ReadWriteProperty { 335 | val testProjectProperty = LateinitProperty("testproject") 336 | var testProject: TestProject by testProjectProperty 337 | beforeEach { 338 | testProject = TestProject() 339 | println("Test project directory: ${testProject.dir}") 340 | } 341 | afterEach { 342 | // comment out if a test is failing and you need to investigate the project files. 343 | testProject.delete() 344 | } 345 | return testProjectProperty 346 | } 347 | 348 | class MiscTest : DynaTest({ 349 | val testProject: TestProject by withTestProject() 350 | 351 | test("something") { 352 | testProject.buildFile.writeText("something") 353 | } 354 | }) 355 | ``` 356 | 357 | To build upon such lazy-init variable (for example to create a test project which comes 358 | pre-populated with some testing files), you have to use the following construct: 359 | 360 | ```kotlin 361 | @DynaTestDsl 362 | fun DynaNodeGroup.withHelloWorldJavaTestProject(): ReadWriteProperty { 363 | val testProjectProperty = withTestProject() 364 | val testProject: TestProject by testProjectProperty 365 | beforeEach { 366 | testProject.buildFile.writeText("""plugins { id 'java' }""") 367 | } 368 | return testProject 369 | } 370 | 371 | group("hello world java examples") { 372 | val project: TestProject by withHelloWorldJavaTestProject() 373 | test("simple") { 374 | project.build("jar") 375 | } 376 | } 377 | ``` 378 | 379 | ## Other utility functions 380 | 381 | * `jvmVersion` variable will return the major JVM version of the current JRE, e.g. 6 for Java 1.6, 8 for Java 8, 11 for Java 11 etc. 382 | 383 | ## Advanced Topics 384 | 385 | ### Conditional tests 386 | 387 | Remember, the block is pure Kotlin code. In fact it is a mini-DSL language, creating tests and groups. 388 | You call the `test()` function to create/register a test; if you don't call the function the test is simply 389 | not created. For example: 390 | 391 | ```kotlin 392 | class NativesTest : DynaTest({ 393 | if (OS.isLinux()) { 394 | test("linux-based test") { 395 | // run tests only on Linux. 396 | } 397 | } 398 | }) 399 | ``` 400 | 401 | The `if (OS.isLinux())` is just a simple Kotlin `if()` followed by a call to the `isLinux()` function. 402 | 403 | ### Disabling tests temporarily 404 | 405 | Use `xtest{}` or `xgroup{}` to temporarily disable a test (since dynatest 0.20): 406 | 407 | ```kotlin 408 | class DisabledTest : DynaTest({ 409 | xtest("not run") {} 410 | xgroup("no child tests are run") { 411 | xtest("not run") {} 412 | test("also not run") {} 413 | } 414 | }) 415 | ``` 416 | 417 | ### Reusable test battery 418 | 419 | Remember that the `test()`/`group()` are just plain Kotlin functions, which register a test or a group. 420 | Typically you call those functions from the block passed into the 421 | `DynaTest` constructor, but you can call them from anywhere - you can extract a reusable function that registers some kind 422 | of reusable battery of tests. The only thing that's needed is the `DynaNodeGroup` receiver (to have a context from 423 | which you can call the `test()`/`group()`/other functions). 424 | 425 | Therefore, in order to create a reusable test battery, you can simply create a (possibly parametrized) function which 426 | runs in the context of the 427 | `DynaNodeGroup`. That allows the function to create test groups and tests as necessary: 428 | 429 | ```kotlin 430 | @DynaTestDsl 431 | fun DynaNodeGroup.layoutTestBattery(clazz: Class) { 432 | group("tests for ${clazz.simpleName}") { 433 | lateinit var layout: ComponentContainer 434 | beforeEach { layout = clazz.newInstance() } 435 | 436 | test("Adding a component will make the count go to 1") { 437 | layout.addComponent(Label("Hello World")) 438 | expect(1) { layout.getComponentCount() } 439 | } 440 | } 441 | } 442 | 443 | class LayoutTest : DynaTest({ 444 | layoutTestBattery(VerticalLayout::class.java) 445 | layoutTestBattery(HorizontalLayout::class.java) 446 | layoutTestBattery(CssLayout::class.java) 447 | layoutTestBattery(FlexLayout::class.java) 448 | }) 449 | ``` 450 | 451 | ### Plugging in into the test life-cycle 452 | 453 | Say that you want to mock the database and clean it before and after every test. Very easy: just extract the 454 | lifecycle-controlling functions into a separate function: 455 | 456 | ```kotlin 457 | @DynaTestDsl 458 | fun DynaNodeGroup.usingDatabase() { 459 | 460 | beforeGroup { 461 | VaadinOnKotlin.dataSourceConfig.apply { 462 | minimumIdle = 0 463 | maximumPoolSize = 30 464 | this.jdbcUrl = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" 465 | this.username = "sa" 466 | this.password = "" 467 | } 468 | Sql2oVOKPlugin().init() 469 | db { 470 | con.createQuery("""create table if not exists Test ( 471 | id bigint primary key auto_increment, 472 | name varchar not null, 473 | age integer not null, 474 | dateOfBirth date, 475 | created timestamp, 476 | alive boolean, 477 | maritalStatus varchar 478 | )""").executeUpdate() 479 | } 480 | } 481 | 482 | afterGroup { 483 | Sql2oVOKPlugin().destroy() 484 | } 485 | 486 | fun clearDatabase() { Person.deleteAll() } 487 | beforeEach { clearDatabase() } 488 | afterEach { clearDatabase() } 489 | } 490 | ``` 491 | 492 | This sample is taken from Vaadin-on-Kotlin [EntityDataProviderTest.kt](https://github.com/mvysny/vaadin-on-kotlin/blob/master/vok-framework-sql2o/src/test/kotlin/com/github/vok/framework/sql2o/vaadin/EntityDataProviderTest.kt) file, 493 | which is somewhat complex. 494 | 495 | Your typical test will look like this: 496 | 497 | ```kotlin 498 | class SomeOtherEntityTest : DynaTest({ 499 | 500 | usingDatabase() 501 | 502 | test("calculating average age") { 503 | db { 504 | for (i in 0..10) { Person(age = i).save() } 505 | } 506 | expect(5) { Person.averageAge() } 507 | } 508 | }) 509 | ``` 510 | 511 | Head to [vok-orm](https://github.com/mvysny/vok-orm) on how to use the database in this fashion from your app. 512 | 513 | ### Real-world Web App Example 514 | 515 | A testing bootstrap in your application will be a lot simpler. See the following example taken from 516 | the [Vaadin Kotlin PWA Demo](https://github.com/mvysny/vaadin-kotlin-pwa): 517 | 518 | ```kotlin 519 | class MainViewTest: DynaTest({ 520 | beforeGroup { Bootstrap().contextInitialized(null) } 521 | afterGroup { Bootstrap().contextDestroyed(null) } 522 | beforeEach { MockVaadin.setup(autoDiscoverViews("com.vaadin.pwademo")) } 523 | beforeEach { Task.deleteAll() } 524 | afterEach { Task.deleteAll() } 525 | 526 | test("add a task") { 527 | UI.getCurrent().navigateTo("") 528 | _get { caption = "Title:" } .value = "New Task" 529 | _get