diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index e3f2667ce..fcf5ebba7 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -11,18 +11,26 @@ object Versions { val logbackClassic = "1.2.3" val axmlParser = "1.0" val bugsnag = "3.6.2" + val asm = "9.1" + val clikt = "3.1.0" + + val grpc = "1.34.1" + val grpcKotlin = "1.0.0" + val protobufGradle = "0.8.14" + val protobuf = "3.14.0" val junitGradle = "1.2.0" val androidGradleVersion = "4.0.0" val junit5 = "5.7.2" + val junit5launcher = "1.7.2" val kluent = "1.65" val kakao = "3.0.2" val espresso = "3.0.1" val espressoRules = "1.0.1" val espressoRunner = "1.0.1" - val junit = "4.12" + val junit = "4.13.2" val gson = "2.8.8" val apacheCommonsText = "1.9" val apacheCommonsIO = "2.9.0" @@ -92,10 +100,18 @@ object Libraries { val allureKotlinCommons = "io.qameta.allure:allure-kotlin-commons:${Versions.allureKotlin}" val koin = "io.insert-koin:koin-core:${Versions.koin}" val bugsnag = "com.bugsnag:bugsnag:${Versions.bugsnag}" + val asm = "org.ow2.asm:asm:${Versions.asm}" + val clikt = "com.github.ajalt.clikt:clikt:${Versions.clikt}" + val protobufLite = "com.google.protobuf:protobuf-javalite:${Versions.protobuf}" + val grpcKotlinStubLite = "io.grpc:grpc-kotlin-stub-lite:${Versions.grpcKotlin}" + val grpcOkhttp = "io.grpc:grpc-okhttp:${Versions.grpc}" + val grpcNetty = "io.grpc:grpc-netty:${Versions.grpc}" } object TestLibraries { val junit5 = "org.junit.jupiter:junit-jupiter:${Versions.junit5}" + val junit5vintage = "org.junit.vintage:junit-vintage-engine:${Versions.junit5}" + val junit5launcher = "org.junit.platform:junit-platform-launcher:${Versions.junit5launcher}" val kluent = "org.amshove.kluent:kluent:${Versions.kluent}" val kakao = "io.github.kakaocup:kakao:${Versions.kakao}" diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts index 141525a10..cf8d8b8cd 100644 --- a/cli/build.gradle.kts +++ b/cli/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(project(":vendor:vendor-android:base")) implementation(project(":vendor:vendor-android:ddmlib")) implementation(project(":vendor:vendor-android:adam")) + implementation(project(":vendor:vendor-junit4:vendor-junit4-core")) implementation(project(":analytics:usage")) implementation(Libraries.kotlinStdLib) implementation(Libraries.kotlinCoroutines) diff --git a/cli/src/main/kotlin/com/malinskiy/marathon/cli/ApplicationView.kt b/cli/src/main/kotlin/com/malinskiy/marathon/cli/ApplicationView.kt index ccc49dd80..f6656a2e3 100644 --- a/cli/src/main/kotlin/com/malinskiy/marathon/cli/ApplicationView.kt +++ b/cli/src/main/kotlin/com/malinskiy/marathon/cli/ApplicationView.kt @@ -40,7 +40,7 @@ fun main(args: Array): Unit = mainBody( .registerModule(JavaTimeModule()) val configuration = ConfigFactory(mapper).create( marathonfile = marathonfile, - environmentReader = SystemEnvironmentReader() + environmentReader = SystemEnvironmentReader(), ) val application = marathonStartKoin(configuration) diff --git a/cli/src/main/kotlin/com/malinskiy/marathon/cli/args/EnvironmentConfiguration.kt b/cli/src/main/kotlin/com/malinskiy/marathon/cli/args/EnvironmentConfiguration.kt index 75d0b94e5..1c6cca0d8 100644 --- a/cli/src/main/kotlin/com/malinskiy/marathon/cli/args/EnvironmentConfiguration.kt +++ b/cli/src/main/kotlin/com/malinskiy/marathon/cli/args/EnvironmentConfiguration.kt @@ -2,4 +2,4 @@ package com.malinskiy.marathon.cli.args import java.io.File -data class EnvironmentConfiguration(val androidSdk: File?) +data class EnvironmentConfiguration(val androidSdk: File?, val javaHome: File?) diff --git a/cli/src/main/kotlin/com/malinskiy/marathon/cli/args/FileJUnit4Configuration.kt b/cli/src/main/kotlin/com/malinskiy/marathon/cli/args/FileJUnit4Configuration.kt new file mode 100644 index 000000000..942a019cc --- /dev/null +++ b/cli/src/main/kotlin/com/malinskiy/marathon/cli/args/FileJUnit4Configuration.kt @@ -0,0 +1,61 @@ +package com.malinskiy.marathon.cli.args + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import com.malinskiy.marathon.vendor.junit4.configuration.Junit4Configuration +import com.malinskiy.marathon.vendor.junit4.configuration.executor.ExecutorConfiguration +import com.malinskiy.marathon.vendor.junit4.model.JUnit4TestBundle +import java.io.File + + +class FileJUnit4Configuration( + @JsonProperty("applicationClasspath") val applicationClasspath: List?, + @JsonProperty("testClasspath") val testClasspath: List?, + @JsonProperty("testPackageRoot") val testPackageRoot: String, + @JsonProperty("source") val source: File? = null, + @JsonProperty("forkEvery") val forkEvery: Int = 1000, + @JsonProperty("debugBooter") val debugBooter: Boolean = false, + @JsonProperty("executorConfiguration") val executorConfiguration: ExecutorConfiguration, + + ) : FileVendorConfiguration { + + fun toJUnit4Configuration( + mapper: ObjectMapper, + javaHome: File? + ): Junit4Configuration { + val testBundles = mutableListOf() + + source?.let { folder -> + for (file in folder.walkTopDown()) { + if (file.isFile) { + val subconfig = mapper.readValue(file.readText()) + testBundles.add( + JUnit4TestBundle( + file.name, + applicationClasspath = subconfig.applicationClasspath, + testClasspath = subconfig.testClasspath, + workdir = subconfig.workdir, + ) + ) + } + } + } + + return Junit4Configuration( + applicationClasspath = mutableListOf().apply { + this@FileJUnit4Configuration.applicationClasspath?.let { addAll(it) } + applicationClasspath?.let { addAll(it) } + }, + testClasspath = mutableListOf().apply { + this@FileJUnit4Configuration.testClasspath?.let { addAll(it) } + testClasspath?.let { addAll(it) } + }, + testPackageRoot = testPackageRoot, + testBundles = testBundles.toList(), + forkEvery = forkEvery, + debugBooter = debugBooter, + executorConfiguration = executorConfiguration, + ) + } +} diff --git a/cli/src/main/kotlin/com/malinskiy/marathon/cli/args/FileJUnit4ModuleConfiguration.kt b/cli/src/main/kotlin/com/malinskiy/marathon/cli/args/FileJUnit4ModuleConfiguration.kt new file mode 100644 index 000000000..67e15bd2a --- /dev/null +++ b/cli/src/main/kotlin/com/malinskiy/marathon/cli/args/FileJUnit4ModuleConfiguration.kt @@ -0,0 +1,10 @@ +package com.malinskiy.marathon.cli.args + +import com.fasterxml.jackson.annotation.JsonProperty +import java.io.File + +class FileJUnit4ModuleConfiguration( + @JsonProperty("workdir") val workdir: String?, + @JsonProperty("applicationClasspath") val applicationClasspath: List?, + @JsonProperty("testClasspath") val testClasspath: List?, +) diff --git a/cli/src/main/kotlin/com/malinskiy/marathon/cli/args/environment/SystemEnvironmentReader.kt b/cli/src/main/kotlin/com/malinskiy/marathon/cli/args/environment/SystemEnvironmentReader.kt index 8a8f70e93..a13245371 100644 --- a/cli/src/main/kotlin/com/malinskiy/marathon/cli/args/environment/SystemEnvironmentReader.kt +++ b/cli/src/main/kotlin/com/malinskiy/marathon/cli/args/environment/SystemEnvironmentReader.kt @@ -4,12 +4,15 @@ import com.malinskiy.marathon.cli.args.EnvironmentConfiguration import java.io.File class SystemEnvironmentReader( - private val environment: (String) -> String? = { System.getenv(it) } + private val environment: (String) -> String? = { System.getenv(it) }, ) : EnvironmentReader { override fun read() = EnvironmentConfiguration( - androidSdk = androidSdkPath()?.let { File(it) } + androidSdk = androidSdkPath()?.let { File(it) }, + javaHome = javaHomePath()?.let { File(it) }, ) private fun androidSdkPath() = environment("ANDROID_HOME") ?: environment("ANDROID_SDK_ROOT") + + private fun javaHomePath() = environment("JAVA_HOME") } diff --git a/cli/src/main/kotlin/com/malinskiy/marathon/cli/config/ConfigFactory.kt b/cli/src/main/kotlin/com/malinskiy/marathon/cli/config/ConfigFactory.kt index df71bce18..1988e337a 100644 --- a/cli/src/main/kotlin/com/malinskiy/marathon/cli/config/ConfigFactory.kt +++ b/cli/src/main/kotlin/com/malinskiy/marathon/cli/config/ConfigFactory.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.malinskiy.marathon.cli.args.FileAndroidConfiguration import com.malinskiy.marathon.cli.args.FileConfiguration import com.malinskiy.marathon.cli.args.FileIOSConfiguration +import com.malinskiy.marathon.cli.args.FileJUnit4Configuration import com.malinskiy.marathon.cli.args.environment.EnvironmentReader import com.malinskiy.marathon.exceptions.ConfigurationException import com.malinskiy.marathon.execution.Configuration @@ -19,7 +20,10 @@ private val logger = MarathonLogging.logger {} class ConfigFactory(private val mapper: ObjectMapper) { private val environmentVariableSubstitutor = StringSubstitutor(StringLookupFactory.INSTANCE.environmentVariableStringLookup()) - fun create(marathonfile: File, environmentReader: EnvironmentReader): Configuration { + fun create( + marathonfile: File, + environmentReader: EnvironmentReader, + ): Configuration { logger.info { "Checking $marathonfile config" } if (!marathonfile.isFile) { @@ -37,6 +41,12 @@ class ConfigFactory(private val mapper: ObjectMapper) { is FileAndroidConfiguration -> { fileVendorConfiguration.toAndroidConfiguration(environmentReader.read().androidSdk) } + is FileJUnit4Configuration -> { + fileVendorConfiguration.toJUnit4Configuration( + mapper, + environmentReader.read().javaHome + ) + } else -> throw ConfigurationException("No vendor config present in ${marathonfile.absolutePath}") } diff --git a/cli/src/main/kotlin/com/malinskiy/marathon/cli/config/DeserializeModule.kt b/cli/src/main/kotlin/com/malinskiy/marathon/cli/config/DeserializeModule.kt index 1a90a705f..badf8a007 100644 --- a/cli/src/main/kotlin/com/malinskiy/marathon/cli/config/DeserializeModule.kt +++ b/cli/src/main/kotlin/com/malinskiy/marathon/cli/config/DeserializeModule.kt @@ -5,6 +5,7 @@ import com.malinskiy.marathon.cli.args.FileVendorConfiguration import com.malinskiy.marathon.cli.config.deserialize.AnalyticsConfigurationDeserializer import com.malinskiy.marathon.cli.config.deserialize.BatchingStrategyDeserializer import com.malinskiy.marathon.cli.config.deserialize.ExecutionTimeSortingStrategyDeserializer +import com.malinskiy.marathon.cli.config.deserialize.FileJUnit4ExecutorConfigurationDeserializer import com.malinskiy.marathon.cli.config.deserialize.FileVendorConfigurationDeserializer import com.malinskiy.marathon.cli.config.deserialize.FixedSizeBatchingStrategyDeserializer import com.malinskiy.marathon.cli.config.deserialize.FlakinessStrategyDeserializer @@ -31,6 +32,7 @@ import com.malinskiy.marathon.execution.strategy.SortingStrategy import com.malinskiy.marathon.execution.strategy.impl.batching.FixedSizeBatchingStrategy import com.malinskiy.marathon.execution.strategy.impl.flakiness.ProbabilityBasedFlakinessStrategy import com.malinskiy.marathon.execution.strategy.impl.sorting.ExecutionTimeSortingStrategy +import com.malinskiy.marathon.vendor.junit4.configuration.executor.ExecutorConfiguration class DeserializeModule(instantTimeProvider: InstantTimeProvider) : SimpleModule() { init { @@ -62,5 +64,6 @@ class DeserializeModule(instantTimeProvider: InstantTimeProvider) : SimpleModule addDeserializer(TestFilter::class.java, TestFilterDeserializer()) addDeserializer(FileVendorConfiguration::class.java, FileVendorConfigurationDeserializer()) addDeserializer(ScreenRecordingPolicy::class.java, ScreenRecordingPolicyDeserializer()) + addDeserializer(ExecutorConfiguration::class.java, FileJUnit4ExecutorConfigurationDeserializer()) } } diff --git a/cli/src/main/kotlin/com/malinskiy/marathon/cli/config/deserialize/FileJUnit4ExecutorConfigurationDeserializer.kt b/cli/src/main/kotlin/com/malinskiy/marathon/cli/config/deserialize/FileJUnit4ExecutorConfigurationDeserializer.kt new file mode 100644 index 000000000..c31e99f9d --- /dev/null +++ b/cli/src/main/kotlin/com/malinskiy/marathon/cli/config/deserialize/FileJUnit4ExecutorConfigurationDeserializer.kt @@ -0,0 +1,45 @@ +package com.malinskiy.marathon.cli.config.deserialize + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.databind.node.ObjectNode +import com.fasterxml.jackson.module.kotlin.treeToValue +import com.malinskiy.marathon.exceptions.ConfigurationException +import com.malinskiy.marathon.vendor.junit4.configuration.executor.DockerExecutorConfiguration +import com.malinskiy.marathon.vendor.junit4.configuration.executor.ExecutorConfiguration +import com.malinskiy.marathon.vendor.junit4.configuration.executor.KubernetesExecutorConfiguration +import com.malinskiy.marathon.vendor.junit4.configuration.executor.LocalExecutorConfiguration + +const val TYPE_LOCAL = "local" +const val TYPE_DOCKER = "docker" +const val TYPE_KUBERNETES = "kubernetes" + +class FileJUnit4ExecutorConfigurationDeserializer : StdDeserializer(ExecutorConfiguration::class.java) { + override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): ExecutorConfiguration { + val codec = p?.codec as ObjectMapper + val node: JsonNode = codec.readTree(p) ?: throw ConfigurationException("Missing JUnit4 executor configuration") + val type = node.get("type").asText() + + return when (type) { + TYPE_LOCAL -> { + (node as ObjectNode).remove("type") + codec.treeToValue(node) ?: throw ConfigurationException("Missing executor configuration") + } + TYPE_DOCKER -> { + (node as ObjectNode).remove("type") + codec.treeToValue(node) ?: throw ConfigurationException("Missing executor configuration") + } + TYPE_KUBERNETES -> { + (node as ObjectNode).remove("type") + codec.treeToValue(node) ?: throw ConfigurationException("Missing executor configuration") + } + else -> throw ConfigurationException( + "Unrecognized executor type $type. " + + "Valid options are $TYPE_LOCAL, $TYPE_DOCKER and $TYPE_KUBERNETES" + ) + } + } +} diff --git a/cli/src/main/kotlin/com/malinskiy/marathon/cli/config/deserialize/FileVendorConfigurationDeserializer.kt b/cli/src/main/kotlin/com/malinskiy/marathon/cli/config/deserialize/FileVendorConfigurationDeserializer.kt index f3d6fff7d..09d0540b6 100644 --- a/cli/src/main/kotlin/com/malinskiy/marathon/cli/config/deserialize/FileVendorConfigurationDeserializer.kt +++ b/cli/src/main/kotlin/com/malinskiy/marathon/cli/config/deserialize/FileVendorConfigurationDeserializer.kt @@ -9,11 +9,13 @@ import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.module.kotlin.treeToValue import com.malinskiy.marathon.cli.args.FileAndroidConfiguration import com.malinskiy.marathon.cli.args.FileIOSConfiguration +import com.malinskiy.marathon.cli.args.FileJUnit4Configuration import com.malinskiy.marathon.cli.args.FileVendorConfiguration import com.malinskiy.marathon.exceptions.ConfigurationException const val TYPE_ANDROID = "Android" const val TYPE_IOS = "iOS" +const val TYPE_JUnit4 = "JUnit4" class FileVendorConfigurationDeserializer : StdDeserializer(FileVendorConfiguration::class.java) { override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): FileVendorConfiguration { @@ -30,9 +32,13 @@ class FileVendorConfigurationDeserializer : StdDeserializer(node) ?: throw ConfigurationException("Missing vendor configuration") } + TYPE_JUnit4 -> { + (node as ObjectNode).remove("type") + codec.treeToValue(node) ?: throw ConfigurationException("Missing vendor configuration") + } else -> throw ConfigurationException( "Unrecognized vendor type $type. " + - "Valid options are $TYPE_ANDROID and $TYPE_IOS" + "Valid options are $TYPE_ANDROID and $TYPE_IOS" ) } } diff --git a/cli/src/test/kotlin/com/malinskiy/marathon/cli/args/EnvironmentConfigurationBuilders.kt b/cli/src/test/kotlin/com/malinskiy/marathon/cli/args/EnvironmentConfigurationBuilders.kt index 480f86de9..3469bbb4a 100644 --- a/cli/src/test/kotlin/com/malinskiy/marathon/cli/args/EnvironmentConfigurationBuilders.kt +++ b/cli/src/test/kotlin/com/malinskiy/marathon/cli/args/EnvironmentConfigurationBuilders.kt @@ -3,7 +3,9 @@ package com.malinskiy.marathon.cli.args import java.io.File fun environmentConfiguration( - androidSdk: File? = null + androidSdk: File? = null, + javaHome: File? = null, ) = EnvironmentConfiguration( - androidSdk = androidSdk + androidSdk = androidSdk, + javaHome = javaHome, ) diff --git a/cli/src/test/kotlin/com/malinskiy/marathon/cli/args/environment/SystemEnvironmentReaderTest.kt b/cli/src/test/kotlin/com/malinskiy/marathon/cli/args/environment/SystemEnvironmentReaderTest.kt index 79cd54f53..adda31e48 100644 --- a/cli/src/test/kotlin/com/malinskiy/marathon/cli/args/environment/SystemEnvironmentReaderTest.kt +++ b/cli/src/test/kotlin/com/malinskiy/marathon/cli/args/environment/SystemEnvironmentReaderTest.kt @@ -2,9 +2,8 @@ package com.malinskiy.marathon.cli.args.environment import com.malinskiy.marathon.cli.args.EnvironmentConfiguration import com.malinskiy.marathon.cli.args.environmentConfiguration -import com.nhaarman.mockitokotlin2.whenever import com.nhaarman.mockitokotlin2.mock -import org.amshove.kluent.shouldBe +import com.nhaarman.mockitokotlin2.whenever import org.amshove.kluent.shouldBeEqualTo import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test diff --git a/cli/src/test/kotlin/com/malinskiy/marathon/cli/config/ConfigFactoryTest.kt b/cli/src/test/kotlin/com/malinskiy/marathon/cli/config/ConfigFactoryTest.kt index 7a25bef1c..49b8b6170 100644 --- a/cli/src/test/kotlin/com/malinskiy/marathon/cli/config/ConfigFactoryTest.kt +++ b/cli/src/test/kotlin/com/malinskiy/marathon/cli/config/ConfigFactoryTest.kt @@ -51,8 +51,8 @@ import org.amshove.kluent.`should be equal to` import org.amshove.kluent.`should be instance of` import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeEmpty +import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldContainSame -import org.amshove.kluent.shouldEqual import org.amshove.kluent.shouldThrow import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -73,7 +73,7 @@ class ConfigFactoryTest { fun mockEnvironmentReader(path: String? = null): EnvironmentReader { val environmentReader = mock() - whenever(environmentReader.read()).thenReturn(EnvironmentConfiguration(path?.let { File(it) })) + whenever(environmentReader.read()).thenReturn(EnvironmentConfiguration(path?.let { File(it) }, null)) return environmentReader } @@ -91,16 +91,16 @@ class ConfigFactoryTest { val file = File(ConfigFactoryTest::class.java.getResource("/fixture/config/sample_1.yaml").file) val configuration = parser.create(file, mockEnvironmentReader()) - configuration.name shouldEqual "sample-app tests" - configuration.outputDir shouldEqual File("./marathon") - configuration.analyticsConfiguration shouldEqual AnalyticsConfiguration.InfluxDbConfiguration( + configuration.name shouldBeEqualTo "sample-app tests" + configuration.outputDir shouldBeEqualTo File("./marathon") + configuration.analyticsConfiguration shouldBeEqualTo AnalyticsConfiguration.InfluxDbConfiguration( url = "http://influx.svc.cluster.local:8086", user = "root", password = "root", dbName = "marathon", retentionPolicyConfiguration = AnalyticsConfiguration.InfluxDbConfiguration.RetentionPolicyConfiguration.default ) - configuration.poolingStrategy shouldEqual ComboPoolingStrategy( + configuration.poolingStrategy shouldBeEqualTo ComboPoolingStrategy( listOf( OmniPoolingStrategy(), ModelPoolingStrategy(), @@ -109,14 +109,14 @@ class ConfigFactoryTest { AbiPoolingStrategy() ) ) - configuration.shardingStrategy shouldEqual CountShardingStrategy(5) - configuration.sortingStrategy shouldEqual SuccessRateSortingStrategy( + configuration.shardingStrategy shouldBeEqualTo CountShardingStrategy(5) + configuration.sortingStrategy shouldBeEqualTo SuccessRateSortingStrategy( Instant.from( DateTimeFormatter.ISO_DATE_TIME.parse("2015-03-14T09:26:53.590Z") ), false ) - configuration.batchingStrategy shouldEqual FixedSizeBatchingStrategy(5) - configuration.flakinessStrategy shouldEqual ProbabilityBasedFlakinessStrategy( + configuration.batchingStrategy shouldBeEqualTo FixedSizeBatchingStrategy(5) + configuration.flakinessStrategy shouldBeEqualTo ProbabilityBasedFlakinessStrategy( 0.7, 3, Instant.from( @@ -125,8 +125,8 @@ class ConfigFactoryTest { ) ) ) - configuration.retryStrategy shouldEqual FixedQuotaRetryStrategy(100, 2) - SimpleClassnameFilter(".*".toRegex()) shouldEqual SimpleClassnameFilter(".*".toRegex()) + configuration.retryStrategy shouldBeEqualTo FixedQuotaRetryStrategy(100, 2) + SimpleClassnameFilter(".*".toRegex()) shouldBeEqualTo SimpleClassnameFilter(".*".toRegex()) configuration.filteringConfiguration.allowlist shouldContainSame listOf( SimpleClassnameFilter(".*".toRegex()), @@ -150,19 +150,19 @@ class ConfigFactoryTest { configuration.testClassRegexes.map { it.toString() } shouldContainSame listOf("^((?!Abstract).)*Test$") // Regex doesn't have proper equals method. Need to check the patter itself - configuration.includeSerialRegexes.joinToString(separator = "") { it.pattern } shouldEqual """emulator-500[2,4]""".toRegex().pattern - configuration.excludeSerialRegexes.joinToString(separator = "") { it.pattern } shouldEqual """emulator-5002""".toRegex().pattern - configuration.ignoreFailures shouldEqual false - configuration.isCodeCoverageEnabled shouldEqual false - configuration.fallbackToScreenshots shouldEqual false - configuration.strictMode shouldEqual true - configuration.testBatchTimeoutMillis shouldEqual 20_000 - configuration.testOutputTimeoutMillis shouldEqual 30_000 - configuration.debug shouldEqual true - configuration.screenRecordingPolicy shouldEqual ScreenRecordingPolicy.ON_ANY - - configuration.deviceInitializationTimeoutMillis shouldEqual 300_000 - configuration.vendorConfiguration shouldEqual AndroidConfiguration( + configuration.includeSerialRegexes.joinToString(separator = "") { it.pattern } shouldBeEqualTo """emulator-500[2,4]""".toRegex().pattern + configuration.excludeSerialRegexes.joinToString(separator = "") { it.pattern } shouldBeEqualTo """emulator-5002""".toRegex().pattern + configuration.ignoreFailures shouldBeEqualTo false + configuration.isCodeCoverageEnabled shouldBeEqualTo false + configuration.fallbackToScreenshots shouldBeEqualTo false + configuration.strictMode shouldBeEqualTo true + configuration.testBatchTimeoutMillis shouldBeEqualTo 20_000 + configuration.testOutputTimeoutMillis shouldBeEqualTo 30_000 + configuration.debug shouldBeEqualTo true + configuration.screenRecordingPolicy shouldBeEqualTo ScreenRecordingPolicy.ON_ANY + + configuration.deviceInitializationTimeoutMillis shouldBeEqualTo 300_000 + configuration.vendorConfiguration shouldBeEqualTo AndroidConfiguration( File("/local/android"), File("kotlin-buildscript/build/outputs/apk/debug/kotlin-buildscript-debug.apk"), File("kotlin-buildscript/build/outputs/apk/androidTest/debug/kotlin-buildscript-debug-androidTest.apk"), @@ -190,7 +190,7 @@ class ConfigFactoryTest { val file = File(ConfigFactoryTest::class.java.getResource("/fixture/config/sample_1_rp.yaml").file) val configuration = parser.create(file, mockEnvironmentReader()) - configuration.analyticsConfiguration shouldEqual AnalyticsConfiguration.InfluxDbConfiguration( + configuration.analyticsConfiguration shouldBeEqualTo AnalyticsConfiguration.InfluxDbConfiguration( url = "http://influx.svc.cluster.local:8086", user = "root", password = "root", @@ -210,32 +210,32 @@ class ConfigFactoryTest { val file = File(ConfigFactoryTest::class.java.getResource("/fixture/config/sample_2.yaml").file) val configuration = parser.create(file, mockEnvironmentReader()) - configuration.name shouldEqual "sample-app tests" - configuration.outputDir shouldEqual File("./marathon") - configuration.analyticsConfiguration shouldEqual AnalyticsConfiguration.DisabledAnalytics - configuration.poolingStrategy shouldEqual OmniPoolingStrategy() - configuration.shardingStrategy shouldEqual ParallelShardingStrategy() - configuration.sortingStrategy shouldEqual NoSortingStrategy() - configuration.batchingStrategy shouldEqual IsolateBatchingStrategy() - configuration.flakinessStrategy shouldEqual IgnoreFlakinessStrategy() - configuration.retryStrategy shouldEqual NoRetryStrategy() - SimpleClassnameFilter(".*".toRegex()) shouldEqual SimpleClassnameFilter(".*".toRegex()) + configuration.name shouldBeEqualTo "sample-app tests" + configuration.outputDir shouldBeEqualTo File("./marathon") + configuration.analyticsConfiguration shouldBeEqualTo AnalyticsConfiguration.DisabledAnalytics + configuration.poolingStrategy shouldBeEqualTo OmniPoolingStrategy() + configuration.shardingStrategy shouldBeEqualTo ParallelShardingStrategy() + configuration.sortingStrategy shouldBeEqualTo NoSortingStrategy() + configuration.batchingStrategy shouldBeEqualTo IsolateBatchingStrategy() + configuration.flakinessStrategy shouldBeEqualTo IgnoreFlakinessStrategy() + configuration.retryStrategy shouldBeEqualTo NoRetryStrategy() + SimpleClassnameFilter(".*".toRegex()) shouldBeEqualTo SimpleClassnameFilter(".*".toRegex()) configuration.filteringConfiguration.allowlist.shouldBeEmpty() configuration.filteringConfiguration.blocklist.shouldBeEmpty() configuration.testClassRegexes.map { it.toString() } shouldContainSame listOf("^((?!Abstract).)*Test[s]*$") - configuration.includeSerialRegexes shouldEqual emptyList() - configuration.excludeSerialRegexes shouldEqual emptyList() - configuration.ignoreFailures shouldEqual false - configuration.isCodeCoverageEnabled shouldEqual false - configuration.fallbackToScreenshots shouldEqual false - configuration.testBatchTimeoutMillis shouldEqual 1800_000 - configuration.testOutputTimeoutMillis shouldEqual 300_000 - configuration.debug shouldEqual true - configuration.screenRecordingPolicy shouldEqual ScreenRecordingPolicy.ON_FAILURE - configuration.vendorConfiguration shouldEqual AndroidConfiguration( + configuration.includeSerialRegexes shouldBeEqualTo emptyList() + configuration.excludeSerialRegexes shouldBeEqualTo emptyList() + configuration.ignoreFailures shouldBeEqualTo false + configuration.isCodeCoverageEnabled shouldBeEqualTo false + configuration.fallbackToScreenshots shouldBeEqualTo false + configuration.testBatchTimeoutMillis shouldBeEqualTo 1800_000 + configuration.testOutputTimeoutMillis shouldBeEqualTo 300_000 + configuration.debug shouldBeEqualTo true + configuration.screenRecordingPolicy shouldBeEqualTo ScreenRecordingPolicy.ON_FAILURE + configuration.vendorConfiguration shouldBeEqualTo AndroidConfiguration( File("/local/android"), File("kotlin-buildscript/build/outputs/apk/debug/kotlin-buildscript-debug.apk"), File("kotlin-buildscript/build/outputs/apk/androidTest/debug/kotlin-buildscript-debug-androidTest.apk"), @@ -256,7 +256,7 @@ class ConfigFactoryTest { val file = File(ConfigFactoryTest::class.java.getResource("/fixture/config/sample_3.yaml").file) val configuration = parser.create(file, mockEnvironmentReader()) - configuration.vendorConfiguration shouldEqual IOSConfiguration( + configuration.vendorConfiguration shouldBeEqualTo IOSConfiguration( derivedDataDir = file.parentFile.resolve("a"), xctestrunPath = file.parentFile.resolve("a/Build/Products/UITesting_iphonesimulator11.0-x86_64.xctestrun"), remoteUsername = "testuser", @@ -278,7 +278,7 @@ class ConfigFactoryTest { val configuration = parser.create(file, mockEnvironmentReader()) val iosConfiguration = configuration.vendorConfiguration as IOSConfiguration - iosConfiguration.remoteRsyncPath shouldEqual "/usr/bin/rsync" + iosConfiguration.remoteRsyncPath shouldBeEqualTo "/usr/bin/rsync" } @Test @@ -295,7 +295,7 @@ class ConfigFactoryTest { val environmentReader = mockEnvironmentReader("/android/home") val configuration = parser.create(file, environmentReader) - configuration.vendorConfiguration shouldEqual AndroidConfiguration( + configuration.vendorConfiguration shouldBeEqualTo AndroidConfiguration( environmentReader.read().androidSdk!!, File("kotlin-buildscript/build/outputs/apk/debug/kotlin-buildscript-debug.apk"), File("kotlin-buildscript/build/outputs/apk/androidTest/debug/kotlin-buildscript-debug-androidTest.apk"), @@ -324,7 +324,7 @@ class ConfigFactoryTest { val file = File(ConfigFactoryTest::class.java.getResource("/fixture/config/sample_8.yaml").file) val configuration = parser.create(file, mockEnvironmentReader()) - configuration.filteringConfiguration.allowlist shouldEqual listOf( + configuration.filteringConfiguration.allowlist shouldBeEqualTo listOf( SimpleClassnameFilter(".*".toRegex()) ) @@ -338,7 +338,7 @@ class ConfigFactoryTest { configuration.filteringConfiguration.allowlist shouldBe emptyList() - configuration.filteringConfiguration.blocklist shouldEqual listOf( + configuration.filteringConfiguration.blocklist shouldBeEqualTo listOf( SimpleClassnameFilter(".*".toRegex()) ) } @@ -350,11 +350,11 @@ class ConfigFactoryTest { configuration.sortingStrategy `should be instance of` ExecutionTimeSortingStrategy::class val sortingStrategy = configuration.sortingStrategy as ExecutionTimeSortingStrategy - sortingStrategy.timeLimit shouldEqual referenceInstant.minus(Duration.ofHours(1)) + sortingStrategy.timeLimit shouldBeEqualTo referenceInstant.minus(Duration.ofHours(1)) configuration.flakinessStrategy `should be instance of` ProbabilityBasedFlakinessStrategy::class val flakinessStrategy = configuration.flakinessStrategy as ProbabilityBasedFlakinessStrategy - flakinessStrategy.timeLimit shouldEqual referenceInstant.minus(Duration.ofDays(30)) + flakinessStrategy.timeLimit shouldBeEqualTo referenceInstant.minus(Duration.ofDays(30)) } @Test diff --git a/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt b/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt index 11c005c4f..8c2c1ad4f 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt @@ -99,8 +99,10 @@ class Marathon( val tests = applyTestFilters(parsedTests) val shard = prepareTestShard(tests, analytics) + for(t in tests) { + log.debug("- ${t.toTestName()}") + } log.info("Scheduling ${tests.size} tests") - log.debug(tests.joinToString(", ") { it.toTestName() }) val currentCoroutineContext = coroutineContext val scheduler = Scheduler( deviceProvider, diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/TestParser.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/TestParser.kt index 5d0ce755c..b31651d71 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/TestParser.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/TestParser.kt @@ -3,5 +3,5 @@ package com.malinskiy.marathon.execution import com.malinskiy.marathon.test.Test interface TestParser { - fun extract(configuration: Configuration): List + suspend fun extract(configuration: Configuration): List } diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/queue/QueueActor.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/queue/QueueActor.kt index d6d8f0d2d..3064b19bc 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/queue/QueueActor.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/queue/QueueActor.kt @@ -22,6 +22,7 @@ import com.malinskiy.marathon.time.Timer import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Job import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.job import java.util.PriorityQueue import java.util.Queue import kotlin.coroutines.CoroutineContext @@ -57,6 +58,22 @@ class QueueActor( init { queue.addAll(testShard.tests + testShard.flakyTests) progressReporter.testCountExpectation(poolId, queue.size) + coroutineContext.job.invokeOnCompletion { + if (activeBatches.isNotEmpty()) { + /** + * This prints out the in-progress batches for hanging build + */ + logger.warn { + "Closing active queue. Tests in progress:\n" + activeBatches.map { + "${it.key}\n${ + it.value.tests.joinToString( + separator = "\n - " + ) { test -> test.toTestName() } + }\n" + } + } + } + } } override suspend fun receive(msg: QueueMessage) { diff --git a/core/src/main/kotlin/com/malinskiy/marathon/report/logs/LogWriter.kt b/core/src/main/kotlin/com/malinskiy/marathon/report/logs/LogWriter.kt index a0892db86..180b36dcf 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/report/logs/LogWriter.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/report/logs/LogWriter.kt @@ -14,8 +14,8 @@ class LogWriter(private val fileManager: FileManager) { } } - fun appendLogs(test: Test, devicePoolId: DevicePoolId, device: DeviceInfo, log: String) { - val logFile = fileManager.createFile(FileType.LOG, devicePoolId, device, test) + fun appendLogs(devicePoolId: DevicePoolId, device: DeviceInfo, log: String) { + val logFile = fileManager.createFile(FileType.LOG, devicePoolId, device) logFile.appendText("$log\n") } } diff --git a/gradle-plugin/gradle-junit4/build.gradle.kts b/gradle-plugin/gradle-junit4/build.gradle.kts new file mode 100644 index 000000000..ee477ac5a --- /dev/null +++ b/gradle-plugin/gradle-junit4/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + `java-gradle-plugin` + `kotlin-dsl` + id("org.jetbrains.dokka") +} + +gradlePlugin { + (plugins) { + create("marathon-junit4") { + id = "marathon-junit4" + implementationClass = "com.malinskiy.marathon.junit4.MarathonPlugin" + } + } +} + +Deployment.initialize(project) + +dependencies { + implementation(gradleApi()) + implementation(Libraries.kotlinLogging) +} diff --git a/gradle-plugin/gradle-junit4/src/main/kotlin/com/malinskiy/marathon/junit4/MarathonExtension.kt b/gradle-plugin/gradle-junit4/src/main/kotlin/com/malinskiy/marathon/junit4/MarathonExtension.kt new file mode 100644 index 000000000..da3cbcf2e --- /dev/null +++ b/gradle-plugin/gradle-junit4/src/main/kotlin/com/malinskiy/marathon/junit4/MarathonExtension.kt @@ -0,0 +1,6 @@ +package com.malinskiy.marathon.junit4 + +import org.gradle.api.Project + +open class MarathonExtension(project: Project) { +} diff --git a/gradle-plugin/gradle-junit4/src/main/kotlin/com/malinskiy/marathon/junit4/MarathonPlugin.kt b/gradle-plugin/gradle-junit4/src/main/kotlin/com/malinskiy/marathon/junit4/MarathonPlugin.kt new file mode 100644 index 000000000..422c29a52 --- /dev/null +++ b/gradle-plugin/gradle-junit4/src/main/kotlin/com/malinskiy/marathon/junit4/MarathonPlugin.kt @@ -0,0 +1,88 @@ +package com.malinskiy.marathon.junit4 + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.plugins.JavaBasePlugin +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.testing.Test +import org.gradle.kotlin.dsl.closureOf +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.findByType +import org.gradle.kotlin.dsl.findPlugin +import org.gradle.kotlin.dsl.getByName + +class MarathonPlugin : Plugin { + + override fun apply(project: Project) { + val extension: MarathonExtension = project.extensions.create("marathon", MarathonExtension::class.java, project) + + project.afterEvaluate { + val javaBasePlugin = + project.plugins.findPlugin(JavaBasePlugin::class) ?: throw IllegalStateException("Java plugin is not found") + + val marathonTask: Task = project.task(TASK_PREFIX, closureOf { + group = JavaBasePlugin.VERIFICATION_GROUP + description = "Runs all the test tasks" + }) + + val javaExtension = + extensions.findByType(JavaPluginExtension::class) ?: throw IllegalStateException("No JavaPluginExtension is found") + + val conf = extensions.getByName("marathon") as? MarathonExtension ?: MarathonExtension(project) + + val sourceSetContainer = + project.extensions.findByType() ?: throw IllegalStateException("No SourceSetContainer is found") + + val mainSourceSet = + sourceSetContainer.findByName(SourceSet.MAIN_SOURCE_SET_NAME) ?: throw IllegalStateException("Sourceset main not found") + + sourceSetContainer.forEach { + when (it.name) { + SourceSet.MAIN_SOURCE_SET_NAME -> Unit + SourceSet.TEST_SOURCE_SET_NAME -> { + val baseTestTask = createTask(project, conf, mainSourceSet, it) + val testTaskDependencies = project.tasks.getByName("test", Test::class).dependsOn + baseTestTask.dependsOn(testTaskDependencies) + marathonTask.dependsOn(baseTestTask) + } + else -> { + logger.warn("Unknown source set ${it.name}") + } + } + } + } + } + + companion object { + private fun createTask( + project: Project, + config: MarathonExtension, + mainSourceSet: SourceSet, + testSourceSet: SourceSet + ): MarathonRunTask { + val marathonTask = project.tasks.create("${TASK_PREFIX}${testSourceSet.name.capitalize()}", MarathonRunTask::class) + + marathonTask.configure(closureOf { + group = JavaBasePlugin.VERIFICATION_GROUP + description = "Runs tests on all the connected devices for '${testSourceSet.name}' " + + "variation and generates a report" + this.mainSourceSet.set(mainSourceSet.name) + this.testSourceSet.set(testSourceSet.name) + outputs.upToDateWhen { false } + + project.tasks.getByName(testSourceSet.name, Test::class) + dependsOn() + }) + + return marathonTask + } + + /** + * Task name prefix. + */ + private const val TASK_PREFIX = "marathon" + } +} diff --git a/gradle-plugin/gradle-junit4/src/main/kotlin/com/malinskiy/marathon/junit4/MarathonRunTask.kt b/gradle-plugin/gradle-junit4/src/main/kotlin/com/malinskiy/marathon/junit4/MarathonRunTask.kt new file mode 100644 index 000000000..6deacdf08 --- /dev/null +++ b/gradle-plugin/gradle-junit4/src/main/kotlin/com/malinskiy/marathon/junit4/MarathonRunTask.kt @@ -0,0 +1,73 @@ +package com.malinskiy.marathon.junit4 + +import org.gradle.api.DefaultTask +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.VerificationTask +import org.gradle.kotlin.dsl.findByType +import org.gradle.kotlin.dsl.property +import java.io.File +import java.io.InputStream +import java.io.PrintStream +import java.util.Scanner +import javax.inject.Inject + + +open class MarathonRunTask @Inject constructor(objects: ObjectFactory) : DefaultTask(), VerificationTask { + @Input + val mainSourceSet: Property = objects.property() + + @Input + val testSourceSet: Property = objects.property() + + private var ignoreFailure: Boolean = false + + @OutputDirectory + var fakeLockingOutput = File(project.rootProject.buildDir, "fake-marathon-locking-output") + + @TaskAction + fun runMarathon() { + val container = project.extensions.findByType() ?: throw IllegalStateException("No SourceSetContainer is found") + val mainClasspath = container.getByName(mainSourceSet.get()).runtimeClasspath + val testClasspath = container.getByName(testSourceSet.get()).runtimeClasspath + + val process = ProcessBuilder("marathon", "--application-classpath", mainClasspath.joinToString(separator = ":") { it.absolutePath }, + "--test-classpath", testClasspath.joinToString(separator = ":") { it.absolutePath }) + .apply { + directory(project.projectDir) + println("Application classpath") + mainClasspath.forEach { + println("- \"${it.absolutePath}\"") + } + println("Test classpath") + testClasspath.forEach { + println("- \"${it.absolutePath}\"") + } + println("Workdir: ${project.projectDir}") + }.start() + + inheritIO(process.inputStream, System.out) + inheritIO(process.errorStream, System.err) + + process.waitFor() + } + + private fun inheritIO(src: InputStream, dest: PrintStream) { + Thread { + val sc = Scanner(src) + while (sc.hasNextLine()) { + dest.println(sc.nextLine()) + } + }.start() + } + + override fun getIgnoreFailures(): Boolean = ignoreFailure + + override fun setIgnoreFailures(ignoreFailures: Boolean) { + ignoreFailure = ignoreFailures + } +} diff --git a/maven-plugin/maven-junit4/marathon-junit4-maven-plugin/pom.xml b/maven-plugin/maven-junit4/marathon-junit4-maven-plugin/pom.xml new file mode 100644 index 000000000..f38c0497e --- /dev/null +++ b/maven-plugin/maven-junit4/marathon-junit4-maven-plugin/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + + + com.malinskiy.marathon + maven-junit4-parent + 0.7.0-SNAPSHOT + + + marathon-junit4-maven-plugin + maven-plugin + JUnit 4 Marathon Maven Plugin + Maven plugin for running JUnit 4 tests using marathon test runner. + + + + org.apache.maven + maven-plugin-api + + + org.apache.maven + maven-core + + + org.apache.maven.plugin-tools + maven-plugin-annotations + provided + + + junit + junit + test + + + org.assertj + assertj-core + test + + + org.apache.maven.plugin-testing + maven-plugin-testing-harness + test + + + org.apache.maven + maven-compat + test + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + + + process-classes + + descriptor + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M2 + + + enforce + + + + + + + enforce + + + + + + + diff --git a/maven-plugin/maven-junit4/marathon-junit4-maven-plugin/src/main/java/com/malinskiy/marathon/plugin/maven/junit4/Classpath.java b/maven-plugin/maven-junit4/marathon-junit4-maven-plugin/src/main/java/com/malinskiy/marathon/plugin/maven/junit4/Classpath.java new file mode 100644 index 000000000..3b9434fd4 --- /dev/null +++ b/maven-plugin/maven-junit4/marathon-junit4-maven-plugin/src/main/java/com/malinskiy/marathon/plugin/maven/junit4/Classpath.java @@ -0,0 +1,177 @@ +package com.malinskiy.marathon.plugin.maven.junit4; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.commons.lang3.SystemUtils; + +import javax.annotation.Nonnull; +import java.io.File; +import java.net.MalformedURLException; +import java.util.*; + +import static java.io.File.pathSeparatorChar; + +/** + * An ordered list of classpath elements with set behaviour + * + * A Classpath is immutable and thread safe. + * + * Immutable and thread safe + * + * @author Kristian Rosenvold + */ +public final class Classpath implements Iterable, Cloneable +{ + private final List unmodifiableElements; + + public static Classpath join( Classpath firstClasspath, Classpath secondClasspath ) + { + LinkedHashSet accumulated = new LinkedHashSet<>(); + if ( firstClasspath != null ) + { + firstClasspath.addTo( accumulated ); + } + if ( secondClasspath != null ) + { + secondClasspath.addTo( accumulated ); + } + return new Classpath( accumulated ); + } + + private void addTo( @Nonnull Collection c ) + { + c.addAll( unmodifiableElements ); + } + + private Classpath() + { + unmodifiableElements = Collections.emptyList(); + } + + public Classpath(@Nonnull Classpath other, @Nonnull String additionalElement ) + { + ArrayList elems = new ArrayList<>( other.unmodifiableElements ); + elems.add( additionalElement ); + unmodifiableElements = Collections.unmodifiableList( elems ); + } + + public Classpath(@Nonnull Collection elements ) + { + List newCp = new ArrayList<>( elements.size() ); + for ( String element : elements ) + { + element = element.trim(); + if ( !element.isEmpty() ) + { + newCp.add( element ); + } + } + unmodifiableElements = Collections.unmodifiableList( newCp ); + } + + public static Classpath emptyClasspath() + { + return new Classpath(); + } + + public Classpath addClassPathElementUrl( String path ) + { + if ( path == null ) + { + throw new IllegalArgumentException( "Null is not a valid class path element url." ); + } + return unmodifiableElements.contains( path ) ? this : new Classpath( this, path ); + } + + @Nonnull + public List getClassPath() + { + return unmodifiableElements; + } + + public void writeToSystemProperty( @Nonnull String propertyName ) + { + StringBuilder sb = new StringBuilder(); + for ( String element : unmodifiableElements ) + { + sb.append( element ) + .append( pathSeparatorChar ); + } + System.setProperty( propertyName, sb.toString() ); + } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) + { + return true; + } + if ( o == null || getClass() != o.getClass() ) + { + return false; + } + + Classpath classpath = (Classpath) o; + + return unmodifiableElements.equals( classpath.unmodifiableElements ); + } + + @Override + public int hashCode() + { + return unmodifiableElements.hashCode(); + } + + public String getLogMessage( @Nonnull String descriptor ) + { + StringBuilder result = new StringBuilder( descriptor ); + for ( String element : unmodifiableElements ) + { + result.append( " " ) + .append( element ); + } + return result.toString(); + } + + public String getCompactLogMessage( @Nonnull String descriptor ) + { + StringBuilder result = new StringBuilder( descriptor ); + for ( String element : unmodifiableElements ) + { + result.append( " " ); + int pos = element.lastIndexOf( File.separatorChar ); + result.append( pos == -1 ? element : element.substring( pos + 1 ) ); + } + return result.toString(); + } + + @Override + public Iterator iterator() + { + return unmodifiableElements.iterator(); + } + + @Override + public Classpath clone() + { + return new Classpath( unmodifiableElements ); + } +} diff --git a/maven-plugin/maven-junit4/marathon-junit4-maven-plugin/src/main/java/com/malinskiy/marathon/plugin/maven/junit4/MarathonMojo.java b/maven-plugin/maven-junit4/marathon-junit4-maven-plugin/src/main/java/com/malinskiy/marathon/plugin/maven/junit4/MarathonMojo.java new file mode 100644 index 000000000..9cb2459bb --- /dev/null +++ b/maven-plugin/maven-junit4/marathon-junit4-maven-plugin/src/main/java/com/malinskiy/marathon/plugin/maven/junit4/MarathonMojo.java @@ -0,0 +1,149 @@ +package com.malinskiy.marathon.plugin.maven.junit4; + +import com.sun.org.apache.bcel.internal.classfile.Unknown; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.MavenProject; + +import java.io.*; +import java.util.HashSet; +import java.util.Scanner; +import java.util.Set; + +import static org.apache.maven.plugins.annotations.LifecyclePhase.VERIFY; + +@SuppressWarnings("UnusedDeclaration") // Used reflectively by Maven. +@Mojo(name = "marathon", defaultPhase = VERIFY, threadSafe = true, requiresDependencyResolution = ResolutionScope.TEST) +public final class MarathonMojo extends AbstractMojo { + @Parameter(defaultValue = "${project}", readonly = true, required = true) + private MavenProject project; + + /** + * The directory containing generated classes of the project being tested. This will be included after the test + * classes in the test classpath. + */ + @Parameter(defaultValue = "${project.build.outputDirectory}") + private File classesDirectory; + + /** + * The directory containing generated test classes of the project being tested. This will be included at the + * beginning of the test classpath. * + */ + @Parameter(defaultValue = "${project.build.testOutputDirectory}") + protected File testClassesDirectory; + + /** + * Additional elements to be appended to the classpath. + */ + @Parameter(property = "maven.test.additionalClasspath") + private String[] additionalClasspathElements; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + Log log = getLog(); + + MavenProject parent = findParent(project); + log.info("Parent: " + parent.getBasedir().getAbsolutePath()); + + Set classpathArtifacts = project.getArtifacts(); + HashSet runtime = new HashSet<>(); + HashSet test = new HashSet<>(); + classpathArtifacts + .stream() + .forEach(a -> { + String scope = a.getScope(); + if (scope.equals("runtime") || scope.equals("compile") || scope.equals("provided")) { + runtime.add(a); + } else if (scope.equals("test")) { + test.add(a); + } else { + log.info("[" + a.getScope() + "] Unknown artifact " + a.getFile().getAbsolutePath() + ""); + } + }); + + TestClassPath testClassPath = new TestClassPath(classpathArtifacts, classesDirectory, + testClassesDirectory, additionalClasspathElements); + + File marathonDir = new File(parent.getBasedir(), ".marathon"); + marathonDir.mkdirs(); + File config = new File(marathonDir, project.getGroupId() + "." + project.getArtifactId()); + + try { + FileWriter writer = new FileWriter(config); + + writer.append("workdir: "); + writer.append(project.getBasedir().getAbsolutePath()); + writer.append("\n"); + writer.append("applicationClasspath:\n"); + writer.append("- " + classesDirectory.getAbsolutePath() + "\n"); + for(Artifact a : runtime) { + writer.append("- " + a.getFile().getAbsolutePath() + "\n"); + } + writer.append("testClasspath:\n"); + writer.append("- " + testClassesDirectory.getAbsolutePath() + "\n"); + for(Artifact a : test) { + writer.append("- " + a.getFile().getAbsolutePath() + "\n"); + } + writer.close(); + + } catch (IOException e) { + e.printStackTrace(); + } + +// log.info("Application classpath"); +// runtime.forEach(a -> log.info("- " + a.getFile().getAbsolutePath())); +// log.info("Test classpath"); +// test.forEach(a -> log.info("- " + a.getFile().getAbsolutePath())); +// log.info("Workdir: " + project.getBasedir().getAbsolutePath()); +// +// ProcessBuilder builder = new ProcessBuilder("marathon", "--application-classpath", classesDirectory.getAbsolutePath() + ":" + joinToString(runtime), +// "--test-classpath", testClassesDirectory.getAbsolutePath() + ":" + joinToString(test)); +// +// Process process = null; +// try { +// process = builder.directory(project.getBasedir()).start(); +// inheritIO(process.getInputStream(), System.out); +// inheritIO(process.getErrorStream(), System.err); +// process.waitFor(); +// } catch (IOException e) { +// e.printStackTrace(); +// } catch (InterruptedException e) { +// e.printStackTrace(); +// } + } + + private MavenProject findParent(MavenProject project) { + MavenProject parent = project; + while(parent.getParent() != null && parent.getParent().getBasedir() != null) { + parent = parent.getParent(); + } + return parent; + } + + private String joinToString(HashSet runtime) { + StringBuilder builder = new StringBuilder(); + runtime.forEach(c -> { + builder.append(c.getFile().getAbsolutePath()); + builder.append(':'); + }); + if(builder.length() > 1) { + builder.deleteCharAt(builder.length() - 1); + } + return builder.toString(); + } + + private void inheritIO(InputStream src, PrintStream dst) { + new Thread(() -> { + Scanner sc = new Scanner(src); + while (sc.hasNextLine()) { + dst.println(sc.nextLine()); + } + }).start(); + } +} diff --git a/maven-plugin/maven-junit4/marathon-junit4-maven-plugin/src/main/java/com/malinskiy/marathon/plugin/maven/junit4/TestClassPath.java b/maven-plugin/maven-junit4/marathon-junit4-maven-plugin/src/main/java/com/malinskiy/marathon/plugin/maven/junit4/TestClassPath.java new file mode 100644 index 000000000..fbb641022 --- /dev/null +++ b/maven-plugin/maven-junit4/marathon-junit4-maven-plugin/src/main/java/com/malinskiy/marathon/plugin/maven/junit4/TestClassPath.java @@ -0,0 +1,90 @@ +package com.malinskiy.marathon.plugin.maven.junit4; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.maven.artifact.Artifact; + +import java.io.File; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.addAll; +import static org.apache.commons.lang3.StringUtils.split; + +final class TestClassPath +{ + private final Iterable artifacts; + private final File classesDirectory; + private final File testClassesDirectory; + private final String[] additionalClasspathElements; + + TestClassPath(Iterable artifacts, + File classesDirectory, + File testClassesDirectory, + String[] additionalClasspathElements ) + { + this.artifacts = artifacts; + this.classesDirectory = classesDirectory; + this.testClassesDirectory = testClassesDirectory; + this.additionalClasspathElements = additionalClasspathElements; + } + + Map getTestDependencies() + { + Map artifactMapping = new LinkedHashMap<>(); + for ( Artifact artifact : artifacts ) + { + artifactMapping.put( artifact.getGroupId() + ":" + artifact.getArtifactId(), artifact ); + } + return artifactMapping; + } + + Classpath toClasspath() + { + List classpath = new ArrayList<>(); + classpath.add( testClassesDirectory.getAbsolutePath() ); + classpath.add( classesDirectory.getAbsolutePath() ); + for ( Artifact artifact : artifacts ) + { + if ( artifact.getArtifactHandler().isAddedToClasspath() ) + { + File file = artifact.getFile(); + if ( file != null ) + { + classpath.add( file.getAbsolutePath() ); + } + } + } + if ( additionalClasspathElements != null ) + { + for ( String additionalClasspathElement : additionalClasspathElements ) + { + if ( additionalClasspathElement != null ) + { + addAll( classpath, split( additionalClasspathElement, "," ) ); + } + } + } + + return new Classpath( classpath ); + } +} diff --git a/maven-plugin/maven-junit4/pom.xml b/maven-plugin/maven-junit4/pom.xml new file mode 100644 index 000000000..60bbf5dcf --- /dev/null +++ b/maven-plugin/maven-junit4/pom.xml @@ -0,0 +1,136 @@ + + + 4.0.0 + + + org.sonatype.oss + oss-parent + 9 + + + com.malinskiy.marathon + maven-junit4-parent + 0.7.0-SNAPSHOT + pom + + JUnit 4 Marathon Maven Plugin (Parent) + Maven plugin for running JUnit 4 tests using marathon test runner. + 2021 + + + marathon-junit4-maven-plugin + + + + + https://github.com/MarathonLabs/marathon + scm:git:git://github.com/MarathonLabs/marathon.git + scm:git:git@github.com:MarathonLabs/marathon.git + + + + + GitHub Issues + https://github.com/MarathonLabs/marathon/issues + + + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + UTF-8 + 1.8 + + + + + + org.apache.maven + maven-plugin-api + 3.6.0 + + + org.apache.maven + maven-core + 3.6.0 + + + org.apache.maven.plugin-tools + maven-plugin-annotations + 3.6.0 + + + junit + junit + 4.13.2 + + + org.assertj + assertj-core + 3.19.0 + + + org.apache.maven.plugin-testing + maven-plugin-testing-harness + 3.3.0 + + + org.apache.maven + maven-compat + 3.6.0 + + + org.apache.maven + maven-artifact + 3.6.3 + + + org.codehaus.plexus + plexus-utils + 3.3.0 + + + org.codehaus.plexus + plexus-component-annotations + 2.1.0 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${java.version} + ${java.version} + + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + 3.6.0 + + true + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + + + diff --git a/sample/android-app/app/src/androidTest/java/com/example/NamedParameterizedTest.kt b/sample/android-app/app/src/androidTest/java/com/example/NamedParameterizedTest.kt new file mode 100644 index 000000000..8f943363e --- /dev/null +++ b/sample/android-app/app/src/androidTest/java/com/example/NamedParameterizedTest.kt @@ -0,0 +1,42 @@ +package com.example + +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +import java.util.Arrays + +import org.junit.Assert.assertEquals + +@RunWith(Parameterized::class) +class NamedParameterizedTest(private val input: Int, private val expected: Int) { + + @Test + fun test() { + assertEquals(expected.toLong(), compute(input).toLong()) + } + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "compute test for input {0}: {1}") + fun data(): Collection> { + return Arrays.asList( + arrayOf(0, 0), + arrayOf(1, 1), + arrayOf(2, 1), + arrayOf(3, 2), + arrayOf(4, 3), + arrayOf(5, 5), + arrayOf(6, 8) + ) + } + + fun compute(n: Int): Int { + return if (n <= 1) { + n + } else { + compute(n - 1) + compute(n - 2) + } + } + } +} diff --git a/sample/gradle-junit4/build.gradle.kts b/sample/gradle-junit4/build.gradle.kts new file mode 100644 index 000000000..cf495158c --- /dev/null +++ b/sample/gradle-junit4/build.gradle.kts @@ -0,0 +1,30 @@ +buildscript { + repositories { + jcenter() + mavenCentral() + google() + mavenLocal() + } + dependencies { + classpath(BuildPlugins.kotlinPlugin) + /** + * Starting with kotlin plugin 1.3.41 coroutines dependency is not propagated to the classpath of gradle plugin + * + * e.g. + * Caused by: java.lang.NoSuchMethodError: kotlinx.coroutines.channels.ChannelIterator.next()Ljava/lang/Object; + * + * Hence we need to explicitly add coroutines to our classpath + */ + classpath(Libraries.kotlinCoroutines) + } +} + +allprojects { + repositories { + maven { url = uri("$rootDir/../build/repository") } + jcenter() + mavenCentral() + google() + mavenLocal() + } +} diff --git a/sample/gradle-junit4/buildSrc/build.gradle.kts b/sample/gradle-junit4/buildSrc/build.gradle.kts new file mode 100644 index 000000000..c39a297b0 --- /dev/null +++ b/sample/gradle-junit4/buildSrc/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + `kotlin-dsl` +} + +repositories { + jcenter() +} \ No newline at end of file diff --git a/sample/gradle-junit4/buildSrc/src/main/kotlin/Versions.kt b/sample/gradle-junit4/buildSrc/src/main/kotlin/Versions.kt new file mode 100644 index 000000000..7fcdbb399 --- /dev/null +++ b/sample/gradle-junit4/buildSrc/src/main/kotlin/Versions.kt @@ -0,0 +1,19 @@ +object Versions { + val kotlin = "1.4.10" + val coroutines = "1.3.9" + + val junit = "4.12" +} + +object BuildPlugins { + val kotlinPlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}" +} + +object Libraries { + val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Versions.kotlin}" + val kotlinCoroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}" +} + +object TestLibraries { + val junit = "junit:junit:${Versions.junit}" +} diff --git a/sample/gradle-junit4/gradle/wrapper/gradle-wrapper.jar b/sample/gradle-junit4/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..457aad0d9 Binary files /dev/null and b/sample/gradle-junit4/gradle/wrapper/gradle-wrapper.jar differ diff --git a/sample/gradle-junit4/gradle/wrapper/gradle-wrapper.properties b/sample/gradle-junit4/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..12d38de6a --- /dev/null +++ b/sample/gradle-junit4/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/sample/gradle-junit4/gradlew b/sample/gradle-junit4/gradlew new file mode 100755 index 000000000..af6708ff2 --- /dev/null +++ b/sample/gradle-junit4/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/sample/gradle-junit4/gradlew.bat b/sample/gradle-junit4/gradlew.bat new file mode 100644 index 000000000..6d57edc70 --- /dev/null +++ b/sample/gradle-junit4/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/sample/gradle-junit4/library/Marathonfile b/sample/gradle-junit4/library/Marathonfile new file mode 100644 index 000000000..c918e93b1 --- /dev/null +++ b/sample/gradle-junit4/library/Marathonfile @@ -0,0 +1,4 @@ +name: "sample-junit4-project tests" +outputDir: "build/reports/marathon" +vendorConfiguration: + type: "JUnit4" diff --git a/sample/gradle-junit4/library/build.gradle.kts b/sample/gradle-junit4/library/build.gradle.kts new file mode 100644 index 000000000..4d23d3f44 --- /dev/null +++ b/sample/gradle-junit4/library/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + `java-library` + id("org.jetbrains.kotlin.jvm") + id("marathon-junit4") version "0.7.0-SNAPSHOT" +} + +dependencies { + implementation(Libraries.kotlinStdLib) + testImplementation(TestLibraries.junit) +} diff --git a/sample/gradle-junit4/library/src/main/kotlin/com/malinskiy/marathon/sample/gradlejunit4/HelloWorldGenerator.kt b/sample/gradle-junit4/library/src/main/kotlin/com/malinskiy/marathon/sample/gradlejunit4/HelloWorldGenerator.kt new file mode 100644 index 000000000..38c222858 --- /dev/null +++ b/sample/gradle-junit4/library/src/main/kotlin/com/malinskiy/marathon/sample/gradlejunit4/HelloWorldGenerator.kt @@ -0,0 +1,5 @@ +package com.malinskiy.marathon.sample.gradlejunit4 + +class HelloWorldGenerator { + fun printHelloWorld() = "Hello world!" +} diff --git a/sample/gradle-junit4/library/src/test/kotlin/com/malinskiy/marathon/sample/gradlejunit4/HelloWorldGeneratorTest.kt b/sample/gradle-junit4/library/src/test/kotlin/com/malinskiy/marathon/sample/gradlejunit4/HelloWorldGeneratorTest.kt new file mode 100644 index 000000000..bf2b31d4d --- /dev/null +++ b/sample/gradle-junit4/library/src/test/kotlin/com/malinskiy/marathon/sample/gradlejunit4/HelloWorldGeneratorTest.kt @@ -0,0 +1,10 @@ +package com.malinskiy.marathon.sample.gradlejunit4 + +import org.junit.Test + +class HelloWorldGeneratorTest { + @Test + fun testHelloWorld() { + assert(HelloWorldGenerator().printHelloWorld() == "Hello world!") + } +} diff --git a/sample/gradle-junit4/settings.gradle.kts b/sample/gradle-junit4/settings.gradle.kts new file mode 100644 index 000000000..9da7256a9 --- /dev/null +++ b/sample/gradle-junit4/settings.gradle.kts @@ -0,0 +1,19 @@ +pluginManagement { + repositories { + google() + maven { url = uri("$rootDir/../../build/repository") } + gradlePluginPortal() + mavenLocal() + } + resolutionStrategy { + eachPlugin { + if (requested.id.id == "marathon-junit4") { + useModule("com.malinskiy.marathon:gradle-junit4:${requested.version}") + } + } + } +} + +rootProject.name = "gradle-junit4" +rootProject.buildFileName = "build.gradle.kts" +include("library") diff --git a/settings.gradle.kts b/settings.gradle.kts index 61d24c94e..4b7f6e36a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -20,7 +20,14 @@ include("vendor:vendor-android:ddmlib") include("vendor:vendor-android:adam") include("vendor:vendor-ios") include("vendor:vendor-test") +include("vendor:vendor-junit4:vendor-junit4-core") +include("vendor:vendor-junit4:vendor-junit4-booter") +include("vendor:vendor-junit4:vendor-junit4-booter-contract") +include("vendor:vendor-junit4:vendor-junit4-runner") +include("vendor:vendor-junit4:vendor-junit4-runner-contract") +include("vendor:vendor-junit4:vendor-junit4-integration-tests") include("marathon-gradle-plugin") +include("gradle-plugin:gradle-junit4") include("report:html-report") include("report:execution-timeline") include("cli") diff --git a/vendor/vendor-android/adam/src/main/kotlin/com/malinskiy/marathon/android/adam/AdamDeviceProvider.kt b/vendor/vendor-android/adam/src/main/kotlin/com/malinskiy/marathon/android/adam/AdamDeviceProvider.kt index 4d14eca6b..258aa3c9f 100644 --- a/vendor/vendor-android/adam/src/main/kotlin/com/malinskiy/marathon/android/adam/AdamDeviceProvider.kt +++ b/vendor/vendor-android/adam/src/main/kotlin/com/malinskiy/marathon/android/adam/AdamDeviceProvider.kt @@ -36,7 +36,7 @@ import java.net.ConnectException import java.util.concurrent.ConcurrentHashMap import kotlin.coroutines.CoroutineContext -private const val DEFAULT_WAIT_FOR_DEVICES_SLEEP_TIME = 500L +const val DEFAULT_WAIT_FOR_DEVICES_SLEEP_TIME = 500L class AdamDeviceProvider( val configuration: Configuration, diff --git a/vendor/vendor-android/adam/src/main/kotlin/com/malinskiy/marathon/android/adam/AndroidDeviceTestRunner.kt b/vendor/vendor-android/adam/src/main/kotlin/com/malinskiy/marathon/android/adam/AndroidDeviceTestRunner.kt index 241ccfef2..9484c1666 100644 --- a/vendor/vendor-android/adam/src/main/kotlin/com/malinskiy/marathon/android/adam/AndroidDeviceTestRunner.kt +++ b/vendor/vendor-android/adam/src/main/kotlin/com/malinskiy/marathon/android/adam/AndroidDeviceTestRunner.kt @@ -161,7 +161,14 @@ class AndroidDeviceTestRunner(private val device: AdamAndroidDevice, private val testBatch: TestBatch ): TestRunnerRequest { val tests = testBatch.tests.map { - "${it.pkg}.${it.clazz}#${it.method}" + if(it.method != "null") { + "${it.pkg}.${it.clazz}#${it.method.bashEscape()}" + } else { + /** + * Special case for tests without any methods + */ + "${it.pkg}.${it.clazz}" + } } logger.debug { "tests = ${tests.toList()}" } @@ -184,6 +191,10 @@ class AndroidDeviceTestRunner(private val device: AdamAndroidDevice, private val } } +private fun String.bashEscape(): String { + return replace(" ", "\\ ") +} + private fun com.malinskiy.adam.request.testrunner.TestIdentifier.toMarathonTestIdentifier(): TestIdentifier { return TestIdentifier(this.className, this.testName) } diff --git a/vendor/vendor-android/adam/src/main/kotlin/com/malinskiy/marathon/android/adam/OnDeviceTestParser.kt b/vendor/vendor-android/adam/src/main/kotlin/com/malinskiy/marathon/android/adam/OnDeviceTestParser.kt new file mode 100644 index 000000000..b87d0d284 --- /dev/null +++ b/vendor/vendor-android/adam/src/main/kotlin/com/malinskiy/marathon/android/adam/OnDeviceTestParser.kt @@ -0,0 +1,109 @@ +package com.malinskiy.marathon.android.adam + +import com.malinskiy.adam.request.testrunner.InstrumentOptions +import com.malinskiy.adam.request.testrunner.TestAssumptionFailed +import com.malinskiy.adam.request.testrunner.TestEnded +import com.malinskiy.adam.request.testrunner.TestEvent +import com.malinskiy.adam.request.testrunner.TestFailed +import com.malinskiy.adam.request.testrunner.TestIgnored +import com.malinskiy.adam.request.testrunner.TestRunEnded +import com.malinskiy.adam.request.testrunner.TestRunFailed +import com.malinskiy.adam.request.testrunner.TestRunStartedEvent +import com.malinskiy.adam.request.testrunner.TestRunStopped +import com.malinskiy.adam.request.testrunner.TestRunnerRequest +import com.malinskiy.adam.request.testrunner.TestStarted +import com.malinskiy.marathon.analytics.internal.pub.Track +import com.malinskiy.marathon.android.AndroidAppInstaller +import com.malinskiy.marathon.android.AndroidConfiguration +import com.malinskiy.marathon.android.AndroidTestBundleIdentifier +import com.malinskiy.marathon.android.model.AndroidTestBundle +import com.malinskiy.marathon.android.model.TestIdentifier +import com.malinskiy.marathon.device.DeviceProvider +import com.malinskiy.marathon.execution.Configuration +import com.malinskiy.marathon.execution.TestParser +import com.malinskiy.marathon.log.MarathonLogging +import com.malinskiy.marathon.test.Test +import com.malinskiy.marathon.time.SystemTimer +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.NonCancellable.isActive +import kotlinx.coroutines.withTimeoutOrNull +import java.time.Clock + +class OnDeviceTestParser(private val testBundleIdentifier: AndroidTestBundleIdentifier) : TestParser { + private val logger = MarathonLogging.logger {} + + override suspend fun extract(configuration: Configuration): List { + val vendorConfiguration = configuration.vendorConfiguration as AndroidConfiguration + val testBundles = vendorConfiguration.testBundlesCompat() + + val provider = AdamDeviceProvider(configuration, vendorConfiguration, Track(), SystemTimer(Clock.systemDefaultZone())) + provider.initialize(vendorConfiguration) + val channel = provider.subscribe() + + try { + for (update in channel) { + if (update is DeviceProvider.DeviceEvent.DeviceConnected) { + val device = update.device as AdamAndroidDevice + return parseTests(device, configuration, vendorConfiguration, testBundles) + } + } + throw RuntimeException("failed to parse") + } finally { + channel.close() + provider.terminate() + } + } + + @OptIn(InternalCoroutinesApi::class) + private suspend fun parseTests( + device: AdamAndroidDevice, + configuration: Configuration, + vendorConfiguration: AndroidConfiguration, + testBundles: List + ): List { + return testBundles.flatMap { bundle -> + val androidTestBundle = AndroidTestBundle(bundle.application, bundle.testApplication) + val instrumentationInfo = androidTestBundle.instrumentationInfo + + val runnerRequest = TestRunnerRequest( + testPackage = instrumentationInfo.instrumentationPackage, + runnerClass = instrumentationInfo.testRunnerClass, + instrumentOptions = InstrumentOptions( + log = true + ), + ) + val androidAppInstaller = AndroidAppInstaller(configuration) + androidAppInstaller.prepareInstallation(device) + val channel = device.executeTestRequest(runnerRequest) + + val tests = mutableListOf() + while (!channel.isClosedForReceive && isActive) { + val events: List? = withTimeoutOrNull(configuration.testOutputTimeoutMillis) { + channel.receiveOrNull() ?: emptyList() + } + if (events == null) { + throw RuntimeException("Unable to parse test list using ${device.serialNumber}") + } else { + for (event in events) { + when (event) { + is TestRunStartedEvent -> Unit + is TestStarted -> { + val test = TestIdentifier(event.id.className, event.id.testName).toTest() + tests.add(test) + testBundleIdentifier.put(test, androidTestBundle) + } + is TestFailed -> Unit + is TestAssumptionFailed -> Unit + is TestIgnored -> Unit + is TestEnded -> Unit + is TestRunFailed -> Unit + is TestRunStopped -> Unit + is TestRunEnded -> Unit + } + } + } + } + tests + } + } +} diff --git a/vendor/vendor-android/adam/src/main/kotlin/com/malinskiy/marathon/android/adam/di/Modules.kt b/vendor/vendor-android/adam/src/main/kotlin/com/malinskiy/marathon/android/adam/di/Modules.kt index 2675575d6..89832d8d1 100644 --- a/vendor/vendor-android/adam/src/main/kotlin/com/malinskiy/marathon/android/adam/di/Modules.kt +++ b/vendor/vendor-android/adam/src/main/kotlin/com/malinskiy/marathon/android/adam/di/Modules.kt @@ -1,9 +1,12 @@ package com.malinskiy.marathon.android.adam.di import com.malinskiy.marathon.android.adam.AdamDeviceProvider +import com.malinskiy.marathon.android.adam.OnDeviceTestParser import com.malinskiy.marathon.device.DeviceProvider +import com.malinskiy.marathon.execution.TestParser import org.koin.dsl.module val adamModule = module { single { AdamDeviceProvider(get(), get(), get(), get()) } + single(override = true) { OnDeviceTestParser(get()) } } diff --git a/vendor/vendor-android/base/src/main/kotlin/com/malinskiy/marathon/android/AndroidTestParser.kt b/vendor/vendor-android/base/src/main/kotlin/com/malinskiy/marathon/android/AndroidTestParser.kt index 8510d9f44..871366eb6 100644 --- a/vendor/vendor-android/base/src/main/kotlin/com/malinskiy/marathon/android/AndroidTestParser.kt +++ b/vendor/vendor-android/base/src/main/kotlin/com/malinskiy/marathon/android/AndroidTestParser.kt @@ -10,7 +10,7 @@ import com.malinskiy.marathon.test.MetaProperty import com.malinskiy.marathon.test.Test class AndroidTestParser(private val testBundleIdentifier: AndroidTestBundleIdentifier) : TestParser { - override fun extract(configuration: Configuration): List { + override suspend fun extract(configuration: Configuration): List { val androidConfiguration = configuration.vendorConfiguration as AndroidConfiguration val testBundles = androidConfiguration.testBundlesCompat() return testBundles.flatMap { bundle -> diff --git a/vendor/vendor-android/base/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/TestRunResultsListener.kt b/vendor/vendor-android/base/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/TestRunResultsListener.kt index 4811475f0..8e1c2ea1f 100644 --- a/vendor/vendor-android/base/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/TestRunResultsListener.kt +++ b/vendor/vendor-android/base/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/TestRunResultsListener.kt @@ -119,6 +119,11 @@ class TestRunResultsListener( } private fun mergeParameterisedResults(results: MutableMap): Map { + /** + * If we explicitly requested parameterized tests - skip merging + */ + if(testBatch.tests.any { it.method.contains('[') && it.method.contains(']') }) return results + val result = mutableMapOf() for (e in results) { val test = e.key diff --git a/vendor/vendor-android/base/src/test/kotlin/com/malinskiy/marathon/android/AndroidTestParserTest.kt b/vendor/vendor-android/base/src/test/kotlin/com/malinskiy/marathon/android/AndroidTestParserTest.kt index 3767d1da6..d917b0496 100644 --- a/vendor/vendor-android/base/src/test/kotlin/com/malinskiy/marathon/android/AndroidTestParserTest.kt +++ b/vendor/vendor-android/base/src/test/kotlin/com/malinskiy/marathon/android/AndroidTestParserTest.kt @@ -2,7 +2,8 @@ package com.malinskiy.marathon.android import com.malinskiy.marathon.execution.Configuration import com.malinskiy.marathon.test.MetaProperty -import org.amshove.kluent.shouldEqual +import kotlinx.coroutines.runBlocking +import org.amshove.kluent.shouldBeEqualTo import org.junit.jupiter.api.Test import java.io.File import com.malinskiy.marathon.test.Test as MarathonTest @@ -46,8 +47,10 @@ class AndroidTestParserTest { @Test fun `should return proper list of test methods`() { - val extractedTests = parser.extract(configuration) - extractedTests shouldEqual listOf( + val extractedTests = runBlocking { + parser.extract(configuration) + } + extractedTests shouldBeEqualTo listOf( MarathonTest( "com.example", "MainActivityTest", "testText", listOf( diff --git a/vendor/vendor-android/base/src/test/kotlin/com/malinskiy/marathon/android/ApkParserTest.kt b/vendor/vendor-android/base/src/test/kotlin/com/malinskiy/marathon/android/ApkParserTest.kt index 8bc629e8f..e554b49e9 100644 --- a/vendor/vendor-android/base/src/test/kotlin/com/malinskiy/marathon/android/ApkParserTest.kt +++ b/vendor/vendor-android/base/src/test/kotlin/com/malinskiy/marathon/android/ApkParserTest.kt @@ -1,6 +1,6 @@ package com.malinskiy.marathon.android -import org.amshove.kluent.shouldEqual +import org.amshove.kluent.shouldBeEqualTo import org.junit.jupiter.api.Test import java.io.File @@ -11,7 +11,7 @@ class ApkParserTest { fun `should parser AndroidManifest and return InstrumentationInfo`() { val parser = ApkParser() val apkFile = File(javaClass.classLoader.getResource("android_test_1.apk").file) - parser.parseInstrumentationInfo(apkFile) shouldEqual InstrumentationInfo( + parser.parseInstrumentationInfo(apkFile) shouldBeEqualTo InstrumentationInfo( "com.example", "com.example.test", "android.support.test.runner.AndroidJUnitRunner" diff --git a/vendor/vendor-android/ddmlib/src/test/kotlin/com/malinskiy/marathon/android/AndroidDeviceProviderTest.kt b/vendor/vendor-android/ddmlib/src/test/kotlin/com/malinskiy/marathon/android/AndroidDeviceProviderTest.kt index 6ee3ef894..75f93021d 100644 --- a/vendor/vendor-android/ddmlib/src/test/kotlin/com/malinskiy/marathon/android/AndroidDeviceProviderTest.kt +++ b/vendor/vendor-android/ddmlib/src/test/kotlin/com/malinskiy/marathon/android/AndroidDeviceProviderTest.kt @@ -6,7 +6,7 @@ import com.malinskiy.marathon.execution.Configuration import com.malinskiy.marathon.time.SystemTimer import ddmlibModule import kotlinx.coroutines.runBlocking -import org.amshove.kluent.shouldEqual +import org.amshove.kluent.shouldBeEqualTo import org.junit.jupiter.api.Test import java.io.File import java.time.Clock @@ -52,7 +52,7 @@ class AndroidDeviceProviderTest { provider.terminate() } - provider.subscribe().isClosedForReceive shouldEqual true - provider.subscribe().isClosedForSend shouldEqual true + provider.subscribe().isClosedForReceive shouldBeEqualTo true + provider.subscribe().isClosedForSend shouldBeEqualTo true } } diff --git a/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSTestParser.kt b/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSTestParser.kt index c5c5fe250..d8a299763 100644 --- a/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSTestParser.kt +++ b/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSTestParser.kt @@ -19,7 +19,7 @@ class IOSTestParser : TestParser { * specified in Marathonfile. When not specified, starts in working directory. Result excludes any tests * marked as skipped in `xctestrun` file. */ - override fun extract(configuration: Configuration): List { + override suspend fun extract(configuration: Configuration): List { val vendorConfiguration = configuration.vendorConfiguration as? IOSConfiguration ?: throw IllegalStateException("Expected IOS configuration") diff --git a/vendor/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/DerivedDataManagerTest.kt b/vendor/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/DerivedDataManagerTest.kt index 911d4ed84..7142a5e6d 100644 --- a/vendor/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/DerivedDataManagerTest.kt +++ b/vendor/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/DerivedDataManagerTest.kt @@ -6,8 +6,8 @@ import com.malinskiy.marathon.log.MarathonLogging import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.reset import com.nhaarman.mockitokotlin2.whenever -import org.amshove.kluent.shouldEqual -import org.amshove.kluent.shouldNotEqual +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldNotBeEqualTo import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeEach @@ -93,7 +93,7 @@ class DerivedDataManagerTest { containerHost = container.containerIpAddress sshPort = container.getMappedPort(22) - sshPort shouldNotEqual 0 + sshPort shouldNotBeEqualTo 0 } @AfterEach @@ -102,7 +102,7 @@ class DerivedDataManagerTest { } companion object { - val logger = MarathonLogging.logger(javaClass.simpleName) + val logger = MarathonLogging.logger {} val privateKey = File(javaClass.classLoader.getResource("fixtures/derived-data-manager/test_rsa").file) @BeforeAll @@ -123,7 +123,7 @@ class DerivedDataManagerTest { fun `should determine products location`() { val manager = DerivedDataManager(configuration = configuration) - manager.productsDir shouldEqual derivedDataDir.resolve("Build/Products/") + manager.productsDir shouldBeEqualTo derivedDataDir.resolve("Build/Products/") } @Test @@ -157,7 +157,7 @@ class DerivedDataManagerTest { val expectedUploadFiles = productsDir.walkTopDown().map { it.relativePathTo(productsDir) }.toSet() - uploadResults shouldEqual expectedUploadFiles + uploadResults shouldBeEqualTo expectedUploadFiles // Download val tempDir = createTempDir() @@ -174,7 +174,7 @@ class DerivedDataManagerTest { productsDir.walkTopDown().map { it.relativePathTo(productsDir) }.toSet() // Compare - tempFiles shouldEqual expectedDownloadFiles + tempFiles shouldBeEqualTo expectedDownloadFiles tempDir.deleteRecursively() } diff --git a/vendor/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/IOSTestParserTest.kt b/vendor/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/IOSTestParserTest.kt index 0aa6d5cfd..e067d425b 100644 --- a/vendor/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/IOSTestParserTest.kt +++ b/vendor/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/IOSTestParserTest.kt @@ -1,10 +1,11 @@ package com.malinskiy.marathon.ios import com.malinskiy.marathon.execution.Configuration -import com.malinskiy.marathon.test.Test as MarathonTest +import kotlinx.coroutines.runBlocking import org.amshove.kluent.shouldContainSame import org.junit.jupiter.api.Test import java.io.File +import com.malinskiy.marathon.test.Test as MarathonTest class IOSTestParserTest { private val parser = IOSTestParser() @@ -54,7 +55,9 @@ class IOSTestParserTest { @Test fun `should return accurate list of tests`() { - val extractedTests = parser.extract(configuration) + val extractedTests = runBlocking { + parser.extract(configuration) + } extractedTests shouldContainSame listOf( MarathonTest("sample-appUITests", "StoryboardTests", "testButton", emptyList()), diff --git a/vendor/vendor-junit4/vendor-junit4-booter-contract/build.gradle.kts b/vendor/vendor-junit4/vendor-junit4-booter-contract/build.gradle.kts new file mode 100644 index 000000000..c8655482e --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-booter-contract/build.gradle.kts @@ -0,0 +1,69 @@ +import com.google.protobuf.gradle.builtins +import com.google.protobuf.gradle.generateProtoTasks +import com.google.protobuf.gradle.id +import com.google.protobuf.gradle.plugins +import com.google.protobuf.gradle.protobuf +import com.google.protobuf.gradle.protoc +import com.google.protobuf.gradle.remove + +plugins { + kotlin("jvm") + id("com.google.protobuf") version Versions.protobufGradle + id("idea") +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${Versions.protobuf}" + } + plugins { + id("java") { + artifact = "io.grpc:protoc-gen-grpc-java:${Versions.grpc}" + } + id("grpc") { + artifact = "io.grpc:protoc-gen-grpc-java:${Versions.grpc}" + } + id("grpckt") { + artifact = "io.grpc:protoc-gen-grpc-kotlin:${Versions.grpcKotlin}:jdk7@jar" + } + } + generateProtoTasks { + all().forEach { + it.builtins { + remove("java") + } + it.plugins { + id("java") { + option("lite") + } + id("grpc") { + option("lite") + } + id("grpckt") { + option("lite") + } + } + } + } +} + + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class) { + kotlinOptions.jvmTarget = "1.8" + kotlinOptions.apiVersion = "1.4" +} + +tasks.withType { duplicatesStrategy = DuplicatesStrategy.INCLUDE } + +dependencies { + implementation(kotlin("stdlib-jdk8", version = Versions.kotlin)) + implementation(Libraries.kotlinCoroutines) + api(Libraries.protobufLite) + api(Libraries.grpcKotlinStubLite) + api(Libraries.grpcOkhttp) +} diff --git a/vendor/vendor-junit4/vendor-junit4-booter-contract/src/main/proto/parser.proto b/vendor/vendor-junit4/vendor-junit4-booter-contract/src/main/proto/parser.proto new file mode 100644 index 000000000..39e2df592 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-booter-contract/src/main/proto/parser.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; + +package com.malinskiy.marathon.vendor.junit4.parser.contract; + +import "google/protobuf/empty.proto"; + +option java_multiple_files = true; +option java_package = "com.malinskiy.marathon.vendor.junit4.parser.contract"; + +message DiscoverRequest { + repeated string applicationClasspath = 1; + repeated string testClasspath = 2; + string rootPackage = 3; +} + +message DiscoverEvent { + EventType eventType = 1; + repeated TestDescription test = 2; +} + +enum EventType { + PARTIAL_PARSE = 0; + FINISHED = 1; +} + +message TestDescription { + string pkg = 1; + string clazz = 2; + string method = 3; + repeated string metaProperties = 4; +} + +service TestParser { + rpc execute(DiscoverRequest) returns (stream DiscoverEvent); + rpc terminate(google.protobuf.Empty) returns (google.protobuf.Empty); +} diff --git a/vendor/vendor-junit4/vendor-junit4-booter-contract/src/main/proto/runner.proto b/vendor/vendor-junit4/vendor-junit4-booter-contract/src/main/proto/runner.proto new file mode 100644 index 000000000..78caf19a1 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-booter-contract/src/main/proto/runner.proto @@ -0,0 +1,50 @@ +syntax = "proto3"; + +package com.malinskiy.marathon.vendor.junit4.booter.contract; + +import "google/protobuf/empty.proto"; + +option java_multiple_files = true; +option java_package = "com.malinskiy.marathon.vendor.junit4.booter.contract"; + +enum EventType { + RUN_STARTED = 0; + RUN_FINISHED = 1; + TEST_STARTED = 2; + TEST_FINISHED = 3; + TEST_FAILURE = 4; + TEST_ASSUMPTION_FAILURE = 5; + TEST_IGNORED = 6; +} + +message TestEvent { + EventType eventType = 1; + sint32 testCount = 2; + sint64 totalDurationMillis = 3; + string classname = 4; + string method = 5; + string message = 6; + string stacktrace = 7; +} + +message TestRequest { + repeated TestDescription testDescription = 1; + TestEnvironment testEnvironment = 2; +} + +message TestEnvironment { + repeated string classpath = 1; + string javaHome = 2; + repeated string javaOptions = 3; + string workdir = 4; +} + +message TestDescription { + string fqtn = 1; + string description = 2; +} + +service TestExecutor { + rpc execute(TestRequest) returns (stream TestEvent); + rpc terminate(google.protobuf.Empty) returns (google.protobuf.Empty); +} diff --git a/vendor/vendor-junit4/vendor-junit4-booter/build.gradle.kts b/vendor/vendor-junit4/vendor-junit4-booter/build.gradle.kts new file mode 100644 index 000000000..c716afc36 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-booter/build.gradle.kts @@ -0,0 +1,31 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + id("org.jetbrains.kotlin.jvm") + id("com.github.johnrengelman.shadow") version "6.1.0" +} + +tasks.processResources.configure { + from(rootProject.project("vendor:vendor-junit4:vendor-junit4-runner").layout.buildDirectory.dir("libs").get().asFile) { + rename { it.replace(".jar", "") } + } + dependsOn(rootProject.project("vendor:vendor-junit4:vendor-junit4-runner").tasks.getByName("shadowJar")) +} + +dependencies { + implementation(Libraries.kotlinStdLib) + implementation(Libraries.kotlinCoroutines) + implementation(TestLibraries.junit) + implementation(TestLibraries.junit5launcher) + implementation(TestLibraries.junit5vintage) + implementation(Libraries.grpcNetty) + implementation(project(":core")) + implementation(project(":vendor:vendor-junit4:vendor-junit4-booter-contract")) + implementation(project(":vendor:vendor-junit4:vendor-junit4-runner-contract")) +} + +tasks.withType { + kotlinOptions.jvmTarget = "1.8" + kotlinOptions.apiVersion = "1.4" +} diff --git a/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/Booter.kt b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/Booter.kt new file mode 100644 index 000000000..316e766d7 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/Booter.kt @@ -0,0 +1,14 @@ +package com.malinskiy.marathon.vendor.junit4.booter + +import com.malinskiy.marathon.vendor.junit4.booter.exec.ExecutionMode +import com.malinskiy.marathon.vendor.junit4.booter.server.BooterServer + +fun main(args: Array) { + val port = System.getenv("PORT")?.toInt() ?: 50051 + val booterMode = System.getenv("MODE")?.toString()?.let { Mode.valueOf(it) } ?: Mode.RUNNER + val executionMode = System.getenv("EXEC_MODE")?.toString()?.let { ExecutionMode.valueOf(it) } ?: ExecutionMode.ISOLATED + + val server = BooterServer(port, booterMode, executionMode) + server.start() + server.blockUntilShutdown() +} diff --git a/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/Mode.kt b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/Mode.kt new file mode 100644 index 000000000..ff8e474db --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/Mode.kt @@ -0,0 +1,6 @@ +package com.malinskiy.marathon.vendor.junit4.booter + +enum class Mode { + RUNNER, //Used for running tests + DISCOVER, //Used for discovering tests +} diff --git a/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/exec/ExecutionMode.kt b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/exec/ExecutionMode.kt new file mode 100644 index 000000000..42ee384b1 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/exec/ExecutionMode.kt @@ -0,0 +1,6 @@ +package com.malinskiy.marathon.vendor.junit4.booter.exec + +enum class ExecutionMode { + INPROCESS, + ISOLATED, +} diff --git a/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/exec/InProcessTestExecutor.kt b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/exec/InProcessTestExecutor.kt new file mode 100644 index 000000000..d8f84ad6d --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/exec/InProcessTestExecutor.kt @@ -0,0 +1,86 @@ +package com.malinskiy.marathon.vendor.junit4.booter.exec + +import com.malinskiy.marathon.vendor.junit4.booter.contract.TestDescription +import com.malinskiy.marathon.vendor.junit4.booter.contract.TestEvent +import com.malinskiy.marathon.vendor.junit4.booter.filter.TestFilter +import com.malinskiy.marathon.vendor.junit4.booter.isolation.ChildFirstURLClassLoader +import com.malinskiy.marathon.vendor.junit4.booter.server.ListenerFlowAdapter +import com.malinskiy.marathon.vendor.junit4.booter.server.switch +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import org.junit.runner.Description +import org.junit.runner.JUnitCore +import org.junit.runner.Request +import org.junit.runner.Result +import org.junit.runner.notification.RunListener +import java.io.File +import java.net.URLClassLoader + +class InProcessTestExecutor : TestExecutor { + private val core = JUnitCore() + + override fun run( + tests: MutableList, + javaHome: String?, + javaOptions: List, + classpathList: MutableList, + workdir: String + ): Flow { + val classloader: URLClassLoader = ChildFirstURLClassLoader( + classpathList.map { File(it).toURI().toURL() }.toTypedArray(), + Thread.currentThread().contextClassLoader + ) + + return callbackFlow { + val actualClassLocator = mutableMapOf() + val callback: RunListener = ListenerFlowAdapter(this, actualClassLocator) + + addCallback(callback) + + classloader.use { + it.switch { + exec(tests, actualClassLocator) + } + } + + awaitClose { + removeCallback(callback) + } + } + } + + override fun terminate() = Unit + + private fun addCallback(callback: RunListener) { + core.addListener(callback) + } + + private fun removeCallback(callback: RunListener) { + core.removeListener(callback) + } + + private fun exec(tests: List, actualClassLocator: MutableMap): Result? { + val klasses = mutableSetOf>() + val testDescriptions = tests.map { test -> + val fqtn = test.fqtn + val klass = fqtn.substringBefore('#') + val loadClass = Class.forName(klass) + klasses.add(loadClass) + + val method = fqtn.substringAfter('#') + Description.createTestDescription(loadClass, method) + }.toHashSet() + + val testFilter = TestFilter(testDescriptions, actualClassLocator) + val request = Request.classes(*klasses.toTypedArray()) + .filterWith(testFilter) + + return try { + core.run(request) + } catch (e: Exception) { + e.printStackTrace() + null + } + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/exec/IsolatedTestExecutor.kt b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/exec/IsolatedTestExecutor.kt new file mode 100644 index 000000000..727facef1 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/exec/IsolatedTestExecutor.kt @@ -0,0 +1,172 @@ +package com.malinskiy.marathon.vendor.junit4.booter.exec + +import com.malinskiy.marathon.vendor.junit4.booter.contract.EventType +import com.malinskiy.marathon.vendor.junit4.booter.contract.TestDescription +import com.malinskiy.marathon.vendor.junit4.booter.contract.TestEvent +import com.malinskiy.marathon.vendor.junit4.runner.contract.Message +import com.malinskiy.marathon.vendor.junit4.runner.contract.TestIdentifier +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import java.io.File +import java.io.FileOutputStream +import java.net.ServerSocket +import java.net.URI +import java.nio.file.Files +import java.nio.file.Paths +import java.util.jar.Attributes +import java.util.jar.JarOutputStream +import java.util.jar.Manifest +import java.util.zip.ZipEntry + +class IsolatedTestExecutor : TestExecutor { + private var backgroundProcess: Process? = null + + override fun run( + tests: MutableList, + javaHome: String?, + javaOptions: List, + classpathList: MutableList, + workdir: String + ): Flow { + return flow { + val socket = ServerSocket(0) + val port = socket.localPort + + /** + * This solution is similar to https://github.com/bazelbuild/bazel/commit/d9a7d3a789be559bd6972208af21adae871d7a44 + */ + val classpathJar = writePathingJarFile(classpathList.map { File(it) }) + val classpath = "$runnerJar:$classpathJar" + + val javaBinary = if (javaHome.isNullOrEmpty()) { + Paths.get("java") + } else { + Paths.get(javaHome, "bin", "java") + } + + val args = mutableListOf().apply { + add("-ea") + addAll(javaOptions) + add("-cp") + add(classpath) + add("com.malinskiy.marathon.vendor.junit4.runner.Runner") + } + + val testList = Files.createTempFile("marathon", "testlist").toFile() + .apply { + outputStream().buffered().use { stream -> + tests.map { + TestIdentifier.newBuilder() + .setFqtn(it.fqtn) + .build() + }.forEach { it.writeDelimitedTo(stream) } + } + } + + backgroundProcess = ProcessBuilder(javaBinary.toString(), *args.toTypedArray()) + .apply { + environment()["OUTPUT"] = port.toString() + environment()["FILTER"] = testList.absolutePath + if (!workdir.isNullOrEmpty()) { + //This will work only locally + directory(File(workdir)) + } + } + .inheritIO() + .start() + + socket.accept().use { + val inputStream = it.getInputStream() + + while (!it.isInputShutdown) { + val message: Message = Message.parseDelimitedFrom(inputStream) ?: break + emit(message.toTestEvent()) + } + + inputStream.close() + } + socket.close() + backgroundProcess?.waitFor() + } + } + + override fun terminate() { + backgroundProcess?.let { + it.destroy() + it.waitFor() + } + } + + private fun writePathingJarFile(classPath: List): File { + val pathingJarFile = File.createTempFile("classpath", ".jar").apply { deleteOnExit() } + FileOutputStream(pathingJarFile).use { fileOutputStream -> + JarOutputStream(fileOutputStream, toManifest(classPath)).use { jarOutputStream -> + jarOutputStream.putNextEntry(ZipEntry("META-INF/")) + } + } + return pathingJarFile + } + + private fun toManifest(classPath: List): Manifest { + val manifest = Manifest() + val attributes = manifest.mainAttributes + attributes[Attributes.Name.MANIFEST_VERSION] = "1.0" + attributes.putValue( + "Class-Path", + classPath.map(File::toURI).map(URI::toString).joinToString(" ") + ) + return manifest + } + + companion object { + val runnerJar: File by lazy { + val tempFile = File.createTempFile("marathon", "runner.jar") + javaClass.getResourceAsStream("/vendor-junit4-runner-all").copyTo(tempFile.outputStream()) + tempFile.deleteOnExit() + tempFile + } + } +} + +private fun Message.toTestEvent(): TestEvent { + return when (type) { + Message.Type.RUN_STARTED -> TestEvent.newBuilder() + .setEventType(EventType.RUN_STARTED) + .build() + Message.Type.RUN_FINISHED -> TestEvent.newBuilder() + .setEventType(EventType.RUN_FINISHED) + .setTotalDurationMillis(totalDurationMillis) + .build() + Message.Type.TEST_STARTED -> TestEvent.newBuilder() + .setEventType(EventType.TEST_STARTED) + .setClassname(classname) + .setMethod(method) + .setTestCount(testCount) + .build() + Message.Type.TEST_FINISHED -> TestEvent.newBuilder() + .setEventType(EventType.TEST_FINISHED) + .setClassname(classname) + .setMethod(method) + .build() + Message.Type.TEST_FAILURE -> TestEvent.newBuilder() + .setEventType(EventType.TEST_FAILURE) + .setClassname(classname) + .setMethod(method) + .setMessage(message) + .setStacktrace(stacktrace) + .build() + Message.Type.TEST_ASSUMPTION_FAILURE -> TestEvent.newBuilder() + .setEventType(EventType.TEST_ASSUMPTION_FAILURE) + .setClassname(classname) + .setMethod(method) + .setMessage(message) + .setStacktrace(stacktrace) + .build() + Message.Type.TEST_IGNORED -> TestEvent.newBuilder() + .setEventType(EventType.TEST_IGNORED) + .setClassname(classname) + .setMethod(method) + .build() + Message.Type.UNRECOGNIZED -> TODO() + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/exec/TestExecutor.kt b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/exec/TestExecutor.kt new file mode 100644 index 000000000..5dfbe4c16 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/exec/TestExecutor.kt @@ -0,0 +1,18 @@ +package com.malinskiy.marathon.vendor.junit4.booter.exec + +import com.malinskiy.marathon.vendor.junit4.booter.contract.TestDescription +import com.malinskiy.marathon.vendor.junit4.booter.contract.TestEvent +import kotlinx.coroutines.flow.Flow + +interface TestExecutor { + fun run( + tests: MutableList, + javaHome: String?, + javaOptions: List, + classpathList: MutableList, + workdir: String + ): Flow + + fun terminate() +} + diff --git a/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/filter/TestFilter.kt b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/filter/TestFilter.kt new file mode 100644 index 000000000..519fb492d --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/filter/TestFilter.kt @@ -0,0 +1,62 @@ +package com.malinskiy.marathon.vendor.junit4.booter.filter + +import org.junit.runner.Description +import org.junit.runner.manipulation.Filter + +class TestFilter( + private val testDescriptions: HashSet, + private val actualClassLocator: MutableMap +) : Filter() { + private val verifiedChildren = mutableSetOf() + + override fun shouldRun(description: Description): Boolean { + println("JUnit asks about $description") + + return if (verifiedChildren.contains(description)) { +// println("Already unfiltered $description before") + true + } else { + shouldRun(description, className = null) + } + } + + fun shouldRun(description: Description, className: String?): Boolean { + if (description.isTest) { + println("$description") + /** + * Handling for parameterized tests that report org.junit.runners.model.TestClass as their test class + */ + val verificationDescription = if (description.className == CLASS_NAME_STUB && className != null) { + Description.createTestDescription(className, description.methodName, *description.annotations.toTypedArray()) + } else { + description + } + val contains = testDescriptions.contains(verificationDescription) + if (contains) { + verifiedChildren.add(description) + if (description.className == CLASS_NAME_STUB && className != null) { + actualClassLocator[description] = className + } + } + + return contains + } + + // explicitly check if any children want to run + var childrenResult = false + for (each in description.children) { +// println("$description") + if (shouldRun(each!!, description.className)) { + childrenResult = true + } + } + + return childrenResult + } + + override fun describe() = "Marathon JUnit4 execution filter" + + companion object { + const val CLASS_NAME_STUB = "org.junit.runners.model.TestClass" + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/isolation/ChildFirstURLClassLoader.kt b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/isolation/ChildFirstURLClassLoader.kt new file mode 100644 index 000000000..3287cf62d --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/isolation/ChildFirstURLClassLoader.kt @@ -0,0 +1,98 @@ +package com.malinskiy.marathon.vendor.junit4.booter.isolation + +import java.io.IOException +import java.io.InputStream +import java.net.URL +import java.net.URLClassLoader +import java.util.Enumeration + +class ChildFirstURLClassLoader(classpath: Array, parent: ClassLoader) : URLClassLoader(classpath, parent) { + private val system: ClassLoader? = getSystemClassLoader() + + @Synchronized + @Throws(ClassNotFoundException::class) + override fun loadClass(name: String, resolve: Boolean): Class<*>? { + var existingClass = findLoadedClass(name) + if (existingClass == null) { + if (system != null) { + try { + existingClass = system.loadClass(name) + } catch (ignored: ClassNotFoundException) { + } + } + if (existingClass == null) { + existingClass = try { + findClass(name) + } catch (e: ClassNotFoundException) { + super.loadClass(name, resolve) + } + } + } + if (resolve) { + resolveClass(existingClass) + } + return existingClass + } + + override fun getResource(name: String): URL { + var url: URL? = null + if (system != null) { + url = system.getResource(name) + } + if (url == null) { + url = findResource(name) + if (url == null) { + url = super.getResource(name) + } + } + return url!! + } + + @Throws(IOException::class) + override fun getResources(name: String): Enumeration { + var systemUrls: Enumeration? = null + if (system != null) { + systemUrls = system.getResources(name) + } + val localUrls = findResources(name) + var parentUrls: Enumeration? = null + if (parent != null) { + parentUrls = parent.getResources(name) + } + val urls: MutableList = ArrayList() + if (systemUrls != null) { + while (systemUrls.hasMoreElements()) { + urls.add(systemUrls.nextElement()) + } + } + if (localUrls != null) { + while (localUrls.hasMoreElements()) { + urls.add(localUrls.nextElement()) + } + } + if (parentUrls != null) { + while (parentUrls.hasMoreElements()) { + urls.add(parentUrls.nextElement()) + } + } + return object : Enumeration { + var iter: Iterator = urls.iterator() + override fun hasMoreElements(): Boolean { + return iter.hasNext() + } + + override fun nextElement(): URL { + return iter.next() + } + } + } + + override fun getResourceAsStream(name: String): InputStream? { + val url = getResource(name) + try { + return if (url != null) url.openStream() else null + } catch (e: IOException) { + } + return null + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/isolation/ClassLoaderExtensions.kt b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/isolation/ClassLoaderExtensions.kt new file mode 100644 index 000000000..489c56f75 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/isolation/ClassLoaderExtensions.kt @@ -0,0 +1,14 @@ +package com.malinskiy.marathon.vendor.junit4.booter.server + +inline fun ClassLoader.switch(block: () -> T): T { + val originalClassLoader = Thread.currentThread().contextClassLoader + return try { + Thread.currentThread().contextClassLoader = this + block.invoke() + } catch (e: Exception) { + e.printStackTrace() + throw e + } finally { + Thread.currentThread().contextClassLoader = originalClassLoader + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/server/BooterServer.kt b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/server/BooterServer.kt new file mode 100644 index 000000000..b0d95b34d --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/server/BooterServer.kt @@ -0,0 +1,75 @@ +package com.malinskiy.marathon.vendor.junit4.booter.server + +import com.malinskiy.marathon.vendor.junit4.booter.Mode +import com.malinskiy.marathon.vendor.junit4.booter.exec.ExecutionMode +import com.malinskiy.marathon.vendor.junit4.booter.exec.InProcessTestExecutor +import com.malinskiy.marathon.vendor.junit4.booter.exec.IsolatedTestExecutor +import io.grpc.Server +import io.grpc.ServerBuilder +import kotlin.system.exitProcess + +class BooterServer(private val port: Int, private val mode: Mode, private val executionMode: ExecutionMode) { + private val server: Server = ServerBuilder + .forPort(port) + .apply { + when (mode) { + Mode.RUNNER -> addService( + TestExecutorService( + when (executionMode) { + ExecutionMode.INPROCESS -> InProcessTestExecutor() + ExecutionMode.ISOLATED -> IsolatedTestExecutor() + } + ) + ) + Mode.DISCOVER -> addService(TestParserService()) + } + } + .maxInboundMessageSize((32 * 1e6).toInt()) + .build() + + fun start() { + try { + withRetry(10, 100) { + server.start() + } + } catch (t: Throwable) { + t.printStackTrace() + exitProcess(1) + } + + println("Server started, listening on $port") + Runtime.getRuntime().addShutdownHook( + Thread { + println("*** JVM is shutting down: shutting down gRPC server") + this@BooterServer.stop() + println("*** server shut down") + } + ) + } + + private fun stop() { + server.shutdown() + } + + fun blockUntilShutdown() { + server.awaitTermination() + } + +} + +@Suppress("TooGenericExceptionCaught") +fun withRetry(attempts: Int, delayTime: Long = 0, f: () -> T): T { + var attempt = 1 + while (true) { + try { + return f() + } catch (th: Throwable) { + if (attempt == attempts) { + throw th + } else { + Thread.sleep(delayTime) + } + } + ++attempt + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/server/ListenerFlowAdapter.kt b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/server/ListenerFlowAdapter.kt new file mode 100644 index 000000000..73b38d287 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/server/ListenerFlowAdapter.kt @@ -0,0 +1,139 @@ +package com.malinskiy.marathon.vendor.junit4.booter.server + +import com.malinskiy.marathon.vendor.junit4.booter.contract.EventType +import com.malinskiy.marathon.vendor.junit4.booter.contract.TestEvent +import com.malinskiy.marathon.vendor.junit4.booter.filter.TestFilter +import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.channels.sendBlocking +import org.junit.runner.Description +import org.junit.runner.Result +import org.junit.runner.notification.Failure +import org.junit.runner.notification.RunListener + +class ListenerFlowAdapter( + private val producer: ProducerScope, + private val actualClassLocator: MutableMap +) : RunListener() { + + override fun testRunStarted(description: Description) { + super.testRunStarted(description) + try { + producer.sendBlocking( + TestEvent.newBuilder() + .setEventType(EventType.RUN_STARTED) + .build() + ) + } catch (e: Exception) { + // Handle exception from the channel: failure in flow or premature closing + } + } + + override fun testRunFinished(result: Result) { + super.testRunFinished(result) + try { + producer.sendBlocking( + TestEvent.newBuilder() + .setEventType(EventType.RUN_FINISHED) + .setTotalDurationMillis(result.runTime) + .build() + ) + } catch (e: Exception) { + // Handle exception from the channel: failure in flow or premature closing + } + producer.close() + } + + override fun testStarted(description: Description) { + super.testStarted(description) + val description = description.toActualDescription(actualClassLocator) + try { + producer.sendBlocking( + TestEvent.newBuilder() + .setEventType(EventType.TEST_STARTED) + .setClassname(description.className) + .setMethod(description.methodName) + .setTestCount(description.testCount()) + .build() + ) + } catch (e: Exception) { + // Handle exception from the channel: failure in flow or premature closing + } + } + + override fun testFinished(description: Description) { + super.testFinished(description) + val description = description.toActualDescription(actualClassLocator) + try { + producer.sendBlocking( + TestEvent.newBuilder() + .setEventType(EventType.TEST_FINISHED) + .setClassname(description.className) + .setMethod(description.methodName) + .build() + ) + } catch (e: Exception) { + // Handle exception from the channel: failure in flow or premature closing + } + } + + override fun testFailure(failure: Failure) { + super.testFailure(failure) + val description = failure.description.toActualDescription(actualClassLocator) +// println(failure.exception.cause?.printStackTrace()) + try { + producer.sendBlocking( + TestEvent.newBuilder() + .setEventType(EventType.TEST_FAILURE) + .setClassname(description.className) + .setMethod(description.methodName) + .setMessage(failure.message) + .setStacktrace(failure.trace) + .build() + ) + } catch (e: Exception) { + // Handle exception from the channel: failure in flow or premature closing + } + } + + override fun testAssumptionFailure(failure: Failure) { + super.testAssumptionFailure(failure) + val description = failure.description.toActualDescription(actualClassLocator) + try { + producer.sendBlocking( + TestEvent.newBuilder() + .setEventType(EventType.TEST_ASSUMPTION_FAILURE) + .setClassname(description.className) + .setMethod(description.methodName) + .setMessage(failure.message) + .setStacktrace(failure.trace) + .build() + ) + } catch (e: Exception) { + // Handle exception from the channel: failure in flow or premature closing + } + } + + override fun testIgnored(description: Description) { + super.testIgnored(description) + val description = description.toActualDescription(actualClassLocator) + try { + producer.sendBlocking( + TestEvent.newBuilder() + .setEventType(EventType.TEST_IGNORED) + .setClassname(description.className) + .setMethod(description.methodName) + .build() + ) + } catch (e: Exception) { + // Handle exception from the channel: failure in flow or premature closing + } + } +} + +private fun Description.toActualDescription(actualClassLocator: MutableMap): Description { + return if (className == TestFilter.CLASS_NAME_STUB) { + Description.createTestDescription(actualClassLocator[this], methodName, *annotations.toTypedArray()) + } else { + this + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/server/TestExecutorService.kt b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/server/TestExecutorService.kt new file mode 100644 index 000000000..ff1561a9f --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/server/TestExecutorService.kt @@ -0,0 +1,28 @@ +package com.malinskiy.marathon.vendor.junit4.booter.server + +import com.google.protobuf.Empty +import com.malinskiy.marathon.vendor.junit4.booter.contract.TestEvent +import com.malinskiy.marathon.vendor.junit4.booter.contract.TestExecutorGrpcKt +import com.malinskiy.marathon.vendor.junit4.booter.contract.TestRequest +import com.malinskiy.marathon.vendor.junit4.booter.exec.TestExecutor +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlin.system.exitProcess + +class TestExecutorService(private val executor: TestExecutor) : TestExecutorGrpcKt.TestExecutorCoroutineImplBase() { + override fun execute(request: TestRequest): Flow { + val tests = request.testDescriptionList + val classpathList = request.testEnvironment.classpathList + val javaHome = request.testEnvironment.javaHome + val javaOptionsList = request.testEnvironment.javaOptionsList + val workdir = request.testEnvironment.workdir + return executor.run(tests, javaHome, javaOptionsList, classpathList, workdir) + .catch { it.printStackTrace() } + } + + override suspend fun terminate(request: Empty): Empty { + executor.terminate() + exitProcess(0) + return super.terminate(request) + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/server/TestParserService.kt b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/server/TestParserService.kt new file mode 100644 index 000000000..209c20160 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-booter/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/server/TestParserService.kt @@ -0,0 +1,173 @@ +package com.malinskiy.marathon.vendor.junit4.booter.server + +import com.google.protobuf.Empty +import com.malinskiy.marathon.test.MetaProperty +import com.malinskiy.marathon.test.Test +import com.malinskiy.marathon.vendor.junit4.parser.contract.DiscoverEvent +import com.malinskiy.marathon.vendor.junit4.parser.contract.DiscoverRequest +import com.malinskiy.marathon.vendor.junit4.parser.contract.EventType +import com.malinskiy.marathon.vendor.junit4.parser.contract.TestDescription +import com.malinskiy.marathon.vendor.junit4.parser.contract.TestParserGrpcKt +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flow +import org.junit.platform.engine.discovery.ClassNameFilter +import org.junit.platform.engine.discovery.DiscoverySelectors +import org.junit.platform.engine.support.descriptor.ClassSource +import org.junit.platform.engine.support.descriptor.MethodSource +import org.junit.platform.launcher.TestIdentifier +import org.junit.platform.launcher.TestPlan +import org.junit.platform.launcher.core.LauncherConfig +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder +import org.junit.platform.launcher.core.LauncherFactory +import org.junit.platform.launcher.listeners.discovery.LauncherDiscoveryListeners +import org.junit.vintage.engine.VintageTestEngine +import java.io.File +import java.net.URLClassLoader +import kotlin.system.exitProcess + +class TestParserService : TestParserGrpcKt.TestParserCoroutineImplBase() { + override fun execute(request: DiscoverRequest): Flow = flow { + val applicationClasspath = request.applicationClasspathList.map { File(it) } + val testClasspath = request.testClasspathList.map { File(it) } + + val cp = mutableListOf().apply { + addAll(testClasspath) + addAll(applicationClasspath) + }.toList() + + val classpathToScan = cp.map { it.toURI().toURL() } + .toTypedArray() + val classloader: ClassLoader = URLClassLoader.newInstance( + classpathToScan, + Thread.currentThread().contextClassLoader + ) + + val plan: TestPlan = classloader.switch { + val launcherConfig = LauncherConfig.builder() + .addTestEngines(VintageTestEngine()) + .enableTestEngineAutoRegistration(false) + .build() + val launcher = LauncherFactory.create(launcherConfig) + + val discoveryRequest = LauncherDiscoveryRequestBuilder() + .selectors( + DiscoverySelectors.selectPackage(request.rootPackage) + ) + .listeners(LauncherDiscoveryListeners.logging()) + .filters( + ClassNameFilter.includeClassNamePatterns(ClassNameFilter.STANDARD_INCLUDE_PATTERN) + ) + .build() + + launcher.discover(discoveryRequest) + } + + if (plan.containsTests()) { + plan.roots.forEach { root: TestIdentifier -> + val tests = plan.getChildren(root) + tests.forEach { test: TestIdentifier -> + when { + test.isContainer -> { + plan.getChildren(test).forEach { method -> + if (method.source.isPresent) { + val source = method.source.get() + when (source) { + is MethodSource -> { + emit(source.toTest().toEvent()) + } + is ClassSource -> { + val testIdentifier = plan.getTestIdentifier(method.uniqueId) + if (plan.getParent(testIdentifier).isPresent) { + val parent = plan.getParent(testIdentifier).get() + val classSource = parent.source.get() as ClassSource + emit(classSource.toTest(source, testIdentifier.displayName).toEvent()) + } else { + println { "Unknown test ${method.uniqueId}" } + } + } + else -> { + println { "Unknown test ${method.uniqueId}" } + } + } + } else if (method.isContainer) { + //Most likely a parameterized test + plan.getChildren(method).forEach { parameterizedTest -> + if (parameterizedTest.source.isPresent) { + val source = parameterizedTest.source.get() + when (source) { + is MethodSource -> { + emit(source.toParameterizedTest(parameterizedTest).toEvent()) + } + else -> { + println { "Unknown test ${parameterizedTest.uniqueId}" } + } + } + } else { + println { "Unknown test ${parameterizedTest.uniqueId}" } + } + } + } else { + println { "Unknown test ${method.uniqueId}" } + } + } + } + } + } + } + } + + emit( + DiscoverEvent.newBuilder().setEventType(EventType.FINISHED).build() + ) + }.catch { it.printStackTrace() } + + override suspend fun terminate(request: Empty): Empty { + exitProcess(0) + return super.terminate(request) + } +} + +private fun Test.toEvent(): DiscoverEvent { + return DiscoverEvent.newBuilder() + .setEventType(EventType.PARTIAL_PARSE) + .addTest(toTestDescription()) + .build() +} + +/** + * Doesn't parse meta values yet + */ +private fun Test.toTestDescription(): TestDescription { + return TestDescription.newBuilder() + .setPkg(pkg) + .setClazz(clazz) + .setMethod(method) + .addAllMetaProperties(metaProperties.map { it.name }) + .build() +} + + +private fun ClassSource.toTest(child: ClassSource, methodName: String): Test { + val clazz = className.substringAfterLast(".") + val pkg = className.substringBeforeLast(".") + return Test(pkg, clazz, methodName, emptyList()) +} + +private fun MethodSource.toTest(): Test { + val clazz = className.substringAfterLast(".") + val pkg = className.substringBeforeLast(".") + val meta = javaMethod.declaredAnnotations.mapNotNull { it.annotationClass.qualifiedName?.let { name -> MetaProperty(name) } } + + javaClass.declaredAnnotations.mapNotNull { it.annotationClass.qualifiedName?.let { name -> MetaProperty(name) } } + + return Test(pkg, clazz, methodName, meta) +} + +private fun MethodSource.toParameterizedTest(parameterizedTest: TestIdentifier): Test { + val clazz = className.substringAfterLast(".") + val pkg = className.substringBeforeLast(".") + val meta = javaMethod.declaredAnnotations.mapNotNull { it.annotationClass.qualifiedName?.let { name -> MetaProperty(name) } } + + javaClass.declaredAnnotations.mapNotNull { it.annotationClass.qualifiedName?.let { name -> MetaProperty(name) } } + + return Test(pkg, clazz, parameterizedTest.displayName, meta) +} diff --git a/vendor/vendor-junit4/vendor-junit4-booter/src/main/resources/logback.xml b/vendor/vendor-junit4/vendor-junit4-booter/src/main/resources/logback.xml new file mode 100644 index 000000000..9b0eb6c69 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-booter/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + diff --git a/vendor/vendor-junit4/vendor-junit4-core/build.gradle.kts b/vendor/vendor-junit4/vendor-junit4-core/build.gradle.kts new file mode 100644 index 000000000..1bf82a286 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/build.gradle.kts @@ -0,0 +1,37 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + id("org.jetbrains.kotlin.jvm") +} + +tasks.processResources.configure { + from(rootProject.project("vendor:vendor-junit4:vendor-junit4-booter").layout.buildDirectory.dir("libs").get().asFile) + dependsOn(rootProject.project("vendor:vendor-junit4:vendor-junit4-booter").tasks.getByName("shadowJar")) +} + +tasks.processTestResources.configure { + from(rootProject.project("vendor:vendor-junit4:vendor-junit4-integration-tests").layout.buildDirectory.dir("libs").get().asFile) + dependsOn(rootProject.project("vendor:vendor-junit4:vendor-junit4-integration-tests").tasks.getByName("testJar")) +} + +dependencies { + implementation(Libraries.kotlinStdLib) + implementation(Libraries.kotlinCoroutines) + implementation(Libraries.kotlinLogging) + implementation(Libraries.logbackClassic) + implementation(TestLibraries.junit) + implementation(TestLibraries.junit5launcher) + implementation(TestLibraries.junit5vintage) + implementation(Libraries.asm) + implementation(project(":core")) + implementation(project(":vendor:vendor-junit4:vendor-junit4-booter-contract")) + + testImplementation(TestLibraries.junit) + testImplementation(TestLibraries.kluent) +} + +tasks.withType { + kotlinOptions.jvmTarget = "1.8" + kotlinOptions.apiVersion = "1.4" +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/Junit4Device.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/Junit4Device.kt new file mode 100644 index 000000000..58ba87a81 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/Junit4Device.kt @@ -0,0 +1,150 @@ +package com.malinskiy.marathon.vendor.junit4 + +import com.malinskiy.marathon.device.Device +import com.malinskiy.marathon.device.DeviceFeature +import com.malinskiy.marathon.device.DevicePoolId +import com.malinskiy.marathon.device.NetworkState +import com.malinskiy.marathon.device.OperatingSystem +import com.malinskiy.marathon.exceptions.DeviceSetupException +import com.malinskiy.marathon.execution.Configuration +import com.malinskiy.marathon.execution.TestBatchResults +import com.malinskiy.marathon.execution.progress.ProgressReporter +import com.malinskiy.marathon.io.FileManager +import com.malinskiy.marathon.report.logs.LogWriter +import com.malinskiy.marathon.test.Test +import com.malinskiy.marathon.test.TestBatch +import com.malinskiy.marathon.time.Timer +import com.malinskiy.marathon.vendor.junit4.booter.Mode +import com.malinskiy.marathon.vendor.junit4.configuration.Junit4Configuration +import com.malinskiy.marathon.vendor.junit4.configuration.executor.LocalExecutorConfiguration +import com.malinskiy.marathon.vendor.junit4.executor.Booter +import com.malinskiy.marathon.vendor.junit4.executor.listener.CompositeTestRunListener +import com.malinskiy.marathon.vendor.junit4.executor.listener.DebugTestRunListener +import com.malinskiy.marathon.vendor.junit4.executor.listener.DeviceLogListener +import com.malinskiy.marathon.vendor.junit4.executor.listener.JUnit4TestRunListener +import com.malinskiy.marathon.vendor.junit4.executor.listener.LineListener +import com.malinskiy.marathon.vendor.junit4.executor.listener.LogListener +import com.malinskiy.marathon.vendor.junit4.executor.listener.ProgressTestRunListener +import com.malinskiy.marathon.vendor.junit4.executor.listener.TestRunResultsListener +import com.malinskiy.marathon.vendor.junit4.executor.local.LocalhostBooter +import com.malinskiy.marathon.vendor.junit4.extensions.isIgnored +import com.malinskiy.marathon.vendor.junit4.install.Junit4AppInstaller +import com.malinskiy.marathon.vendor.junit4.model.TestIdentifier +import kotlinx.coroutines.CompletableDeferred +import java.io.File +import java.util.UUID + +class Junit4Device( + configuration: Configuration, + protected val timer: Timer, + private val testBundleIdentifier: Junit4TestBundleIdentifier, + private val controlPort: Int = 50051 +) : Device { + override val operatingSystem: OperatingSystem = OperatingSystem(System.getProperty("os.name") ?: "") + override val serialNumber: String = UUID.randomUUID().toString() + override val model: String = System.getProperty("java.version") + override val manufacturer: String = System.getProperty("java.vendor") + override val networkState: NetworkState = NetworkState.CONNECTED + override val deviceFeatures: Collection = emptySet() + override val healthy: Boolean = true + override val abi: String = System.getProperty("os.arch") + private var forkEvery: Int = 1000 + private var current: Int = 0 + + private val fileManager = FileManager(configuration.outputDir) + private val logWriter = LogWriter(fileManager) + + private lateinit var booter: Booter + private var deviceLogListener: DeviceLogListener? = null + + override suspend fun prepare(configuration: Configuration) { + val conf = configuration.vendorConfiguration as Junit4Configuration + + forkEvery = conf.forkEvery + + booter = when (conf.executorConfiguration) { + is LocalExecutorConfiguration -> { + LocalhostBooter(conf, controlPort, Mode.RUNNER) + } + else -> { + throw DeviceSetupException("Unsupported executor configuration ${conf.executorConfiguration.javaClass.simpleName}") + } + }.apply { + prepare() + } + + val installer = Junit4AppInstaller(conf) + installer.install() + } + + override suspend fun execute( + configuration: Configuration, + devicePoolId: DevicePoolId, + rawTestBatch: TestBatch, + deferred: CompletableDeferred, + progressReporter: ProgressReporter + ) { + if (configuration.debug && deviceLogListener == null) { + val logListener = DeviceLogListener(this, devicePoolId, logWriter) + deviceLogListener = logListener + addLogListener(logListener) + } + + if (current >= forkEvery) { + booter.recreate() + current = 0 + } + + val listener = CompositeTestRunListener( + listOf( + DebugTestRunListener(this, rawTestBatch.tests), + ProgressTestRunListener(this, devicePoolId, progressReporter), + TestRunResultsListener(rawTestBatch, this, deferred, timer, emptyList()), + LogListener(this, devicePoolId, rawTestBatch.id, logWriter), + ) + ) + + val ignoredTests = rawTestBatch.tests.filter { test -> test.isIgnored() } + val testBatch = TestBatch(rawTestBatch.tests - ignoredTests, rawTestBatch.id) + if (testBatch.tests.isEmpty()) { + notifyIgnoredTest(ignoredTests, listener) + listener.testRunEnded(0, emptyMap()) + return + } + + val applicationClasspath = mutableSetOf() + val testClasspath = mutableSetOf() + var workdir: String? = null + testBatch.tests.forEach { + val bundle = testBundleIdentifier.identify(it) + bundle.applicationClasspath?.let { list -> applicationClasspath.addAll(list) } + bundle.testClasspath?.let { list -> testClasspath.addAll(list) } + bundle.workdir?.let { dir -> workdir = dir } + } + + notifyIgnoredTest(ignoredTests, listener) + booter.testExecutorClient!!.execute(testBatch.tests, applicationClasspath.toList(), testClasspath.toList(), workdir, listener) + current += testBatch.tests.size + } + + override fun dispose() { + booter.dispose() + } + + fun addLogListener(logListener: LineListener) { + booter.addLogListener(logListener) + } + + fun removeLogListener(logListener: LineListener) { + booter.removeLogListener(logListener) + } + + private suspend fun notifyIgnoredTest(ignoredTests: List, listeners: JUnit4TestRunListener) { + ignoredTests.forEach { + val identifier = TestIdentifier("${it.pkg}.${it.clazz}", it.method) + listeners.testStarted(identifier) + listeners.testIgnored(identifier) + listeners.testEnded(identifier, hashMapOf()) + } + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/Junit4DeviceProvider.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/Junit4DeviceProvider.kt new file mode 100644 index 000000000..c90a22fd6 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/Junit4DeviceProvider.kt @@ -0,0 +1,52 @@ +package com.malinskiy.marathon.vendor.junit4 + +import com.malinskiy.marathon.actor.unboundedChannel +import com.malinskiy.marathon.device.DeviceProvider +import com.malinskiy.marathon.execution.Configuration +import com.malinskiy.marathon.time.Timer +import com.malinskiy.marathon.vendor.VendorConfiguration +import com.malinskiy.marathon.vendor.junit4.configuration.Junit4Configuration +import com.malinskiy.marathon.vendor.junit4.configuration.executor.ExecutorConfigurationAdapter +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.runBlocking +import java.util.concurrent.ConcurrentHashMap + +class Junit4DeviceProvider( + private val configuration: Configuration, + private val timer: Timer +) : DeviceProvider { + override val deviceInitializationTimeoutMillis: Long = configuration.deviceInitializationTimeoutMillis + + private val channel: Channel = unboundedChannel() + private val devices: MutableMap = ConcurrentHashMap() + private lateinit var testBundleIdentifier: Junit4TestBundleIdentifier + private var parallelism: Int = 0 + + override suspend fun initialize(vendorConfiguration: VendorConfiguration) { + val junit4Configuration = vendorConfiguration as Junit4Configuration + val executorConfiguration = junit4Configuration.executorConfiguration as ExecutorConfigurationAdapter + testBundleIdentifier = junit4Configuration.testBundleIdentifier() as Junit4TestBundleIdentifier + parallelism = executorConfiguration.parallelism + } + + override suspend fun terminate() { + devices.forEach { (_, device) -> + device.dispose() + } + channel.close() + } + + override fun subscribe(): Channel { + runBlocking { + val count = parallelism + for (i in 0 until count) { + //For now only local instance is supported + val localhost = Junit4Device(configuration, timer, testBundleIdentifier, controlPort = 50051 + i) + devices["localhost-$i"] = localhost + channel.send(DeviceProvider.DeviceEvent.DeviceConnected(localhost)) + } + } + + return channel + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/Junit4TestBundleIdentifier.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/Junit4TestBundleIdentifier.kt new file mode 100644 index 000000000..96d1544cb --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/Junit4TestBundleIdentifier.kt @@ -0,0 +1,21 @@ +package com.malinskiy.marathon.vendor.junit4 + +import com.malinskiy.marathon.exceptions.ConfigurationException +import com.malinskiy.marathon.execution.bundle.TestBundleIdentifier +import com.malinskiy.marathon.test.Test +import com.malinskiy.marathon.test.toHumanReadableTestName +import com.malinskiy.marathon.vendor.junit4.model.JUnit4TestBundle +import java.util.concurrent.ConcurrentHashMap + +class Junit4TestBundleIdentifier : TestBundleIdentifier { + private val testToBundle = ConcurrentHashMap() + + override fun identify(test: Test): JUnit4TestBundle { + return testToBundle[test] + ?: throw ConfigurationException("Invalid test ${test.toHumanReadableTestName()}: can't locate test bundle") + } + + fun put(test: Test, testBundle: JUnit4TestBundle) { + testToBundle[test] = testBundle + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/Mode.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/Mode.kt new file mode 100644 index 000000000..ff8e474db --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/booter/Mode.kt @@ -0,0 +1,6 @@ +package com.malinskiy.marathon.vendor.junit4.booter + +enum class Mode { + RUNNER, //Used for running tests + DISCOVER, //Used for discovering tests +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/client/TestDiscoveryClient.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/client/TestDiscoveryClient.kt new file mode 100644 index 000000000..7bb871707 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/client/TestDiscoveryClient.kt @@ -0,0 +1,54 @@ +package com.malinskiy.marathon.vendor.junit4.client + +import com.malinskiy.marathon.test.MetaProperty +import com.malinskiy.marathon.test.Test +import com.malinskiy.marathon.vendor.junit4.parser.contract.DiscoverEvent +import com.malinskiy.marathon.vendor.junit4.parser.contract.DiscoverRequest +import com.malinskiy.marathon.vendor.junit4.parser.contract.TestParserGrpcKt +import io.grpc.ManagedChannel +import kotlinx.coroutines.flow.collect +import java.io.Closeable +import java.io.File +import java.util.concurrent.TimeUnit + +class TestDiscoveryClient( + private val channel: ManagedChannel +) : Closeable { + private val stub: TestParserGrpcKt.TestParserCoroutineStub = + TestParserGrpcKt.TestParserCoroutineStub(channel) + .withWaitForReady() + .withMaxInboundMessageSize((32 * 1e6).toInt()) + .withMaxOutboundMessageSize((32 * 1e6).toInt()) + + suspend fun execute(rootPackage: String, applicationClasspath: List, testClasspath: List): List { + val request = DiscoverRequest.newBuilder() + .addAllApplicationClasspath(applicationClasspath.map { it.absolutePath }) + .addAllTestClasspath(testClasspath.map { it.absolutePath }) + .setRootPackage(rootPackage) + .build() + + val responseFlow = stub.execute(request) + val response = mutableListOf() + responseFlow.collect { event: DiscoverEvent -> + when (event.eventType) { + com.malinskiy.marathon.vendor.junit4.parser.contract.EventType.PARTIAL_PARSE -> { + val tests: List = event.testList.map { description -> + Test( + description.pkg, + description.clazz, + description.method, + description.metaPropertiesList.map { MetaProperty(it) }) + } + response.addAll(tests) + } + com.malinskiy.marathon.vendor.junit4.parser.contract.EventType.FINISHED -> Unit + com.malinskiy.marathon.vendor.junit4.parser.contract.EventType.UNRECOGNIZED -> Unit + } + } + return response + } + + override fun close() { + channel.shutdown().awaitTermination(10, TimeUnit.SECONDS) + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/client/TestExecutorClient.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/client/TestExecutorClient.kt new file mode 100644 index 000000000..d84a5d650 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/client/TestExecutorClient.kt @@ -0,0 +1,101 @@ +package com.malinskiy.marathon.vendor.junit4.client + +import com.malinskiy.marathon.test.Test +import com.malinskiy.marathon.test.toTestName +import com.malinskiy.marathon.vendor.junit4.booter.contract.EventType +import com.malinskiy.marathon.vendor.junit4.booter.contract.TestDescription +import com.malinskiy.marathon.vendor.junit4.booter.contract.TestEnvironment +import com.malinskiy.marathon.vendor.junit4.booter.contract.TestEvent +import com.malinskiy.marathon.vendor.junit4.booter.contract.TestExecutorGrpcKt +import com.malinskiy.marathon.vendor.junit4.booter.contract.TestRequest +import com.malinskiy.marathon.vendor.junit4.configuration.executor.ExecutorConfiguration +import com.malinskiy.marathon.vendor.junit4.executor.listener.JUnit4TestRunListener +import com.malinskiy.marathon.vendor.junit4.model.TestIdentifier +import io.grpc.ManagedChannel +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import java.io.Closeable +import java.io.File +import java.util.concurrent.TimeUnit + +class TestExecutorClient( + private val channel: ManagedChannel, + private val executorConfiguration: ExecutorConfiguration +) : Closeable { + private val stub: TestExecutorGrpcKt.TestExecutorCoroutineStub = + TestExecutorGrpcKt.TestExecutorCoroutineStub(channel) + .withWaitForReady() + .withMaxInboundMessageSize((32 * 1e6).toInt()) + .withMaxOutboundMessageSize((32 * 1e6).toInt()) + + suspend fun execute( + tests: List, + applicationClasspath: List, + testClasspath: List, + workdirectory: String?, + listener: JUnit4TestRunListener + ) { + val descriptions = tests.map { + TestDescription.newBuilder() + .apply { + fqtn = it.toTestName() + } + .build() + } + + val testEnvironment = TestEnvironment.newBuilder() + .addAllClasspath(testClasspath.map { it.absolutePath }) + .addAllClasspath(applicationClasspath.map { it.absolutePath }) + .apply { + if (executorConfiguration.debug) { + addJavaOptions("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=1045") + } + addAllJavaOptions(executorConfiguration.javaOptions) + executorConfiguration.javaHome?.let { + javaHome = it.absolutePath + } + workdirectory?.let { + workdir = it + } + } + .build() + + val request = TestRequest.newBuilder() + .addAllTestDescription(descriptions) + .setTestEnvironment(testEnvironment) + .build() + + val responseFlow = stub.execute(request) + responseFlow.catch { it.printStackTrace() } + .collect { event: TestEvent -> + when (event.eventType) { + EventType.RUN_STARTED -> { + listener.testRunStarted("Marathon JUnit4 Test Run", event.testCount) + } + EventType.RUN_FINISHED -> { + listener.testRunEnded(event.totalDurationMillis, emptyMap()) + } + EventType.TEST_STARTED -> { + listener.testStarted(TestIdentifier(event.classname, event.method)) + } + EventType.TEST_FINISHED -> { + listener.testEnded(TestIdentifier(event.classname, event.method), emptyMap()) + } + EventType.TEST_FAILURE -> { + listener.testFailed(TestIdentifier(event.classname, event.method), event.stacktrace) + } + EventType.TEST_ASSUMPTION_FAILURE -> { + listener.testAssumptionFailure(TestIdentifier(event.classname, event.method), event.stacktrace) + } + EventType.TEST_IGNORED -> { + listener.testIgnored(TestIdentifier(event.classname, event.method)) + } + EventType.UNRECOGNIZED -> Unit + } + } + } + + override fun close() { + channel.shutdown().awaitTermination(10, TimeUnit.SECONDS) + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/Junit4Configuration.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/Junit4Configuration.kt new file mode 100644 index 000000000..2e9a6a810 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/Junit4Configuration.kt @@ -0,0 +1,58 @@ +package com.malinskiy.marathon.vendor.junit4.configuration + +import com.malinskiy.marathon.execution.bundle.TestBundleIdentifier +import com.malinskiy.marathon.log.MarathonLogConfigurator +import com.malinskiy.marathon.vendor.VendorConfiguration +import com.malinskiy.marathon.vendor.junit4.Junit4DeviceProvider +import com.malinskiy.marathon.vendor.junit4.Junit4TestBundleIdentifier +import com.malinskiy.marathon.vendor.junit4.configuration.executor.ExecutorConfiguration +import com.malinskiy.marathon.vendor.junit4.model.JUnit4TestBundle +import com.malinskiy.marathon.vendor.junit4.parsing.RemoteJupiterTestParser +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import org.koin.dsl.module +import java.io.File + +/** + * @param useArgfiles some JDK version do not support argfiles. Defaults to true + */ +data class Junit4Configuration( + val applicationClasspath: List?, + val testClasspath: List?, + val testBundles: List?, + val testPackageRoot: String? = null, + val debugBooter: Boolean = false, + val forkEvery: Int = 1000, + val executorConfiguration: ExecutorConfiguration, +) : VendorConfiguration, KoinComponent { + override fun logConfigurator(): MarathonLogConfigurator = Junit4LogConfigurator() + + override fun testParser() = RemoteJupiterTestParser(get()) + + override fun deviceProvider() = Junit4DeviceProvider(get(), get()) + + override fun testBundleIdentifier(): TestBundleIdentifier = get() + + override fun modules() = listOf( + module { + val testBundleIdentifier = Junit4TestBundleIdentifier() + single { testBundleIdentifier } + single { testBundleIdentifier } + } + ) + + fun testBundlesCompat(): List { + return mutableListOf().apply { + testBundles?.let { addAll(it) } + if (!testClasspath.isNullOrEmpty()) { + add( + JUnit4TestBundle( + id = "main", + applicationClasspath = applicationClasspath, + testClasspath = testClasspath, + ) + ) + } + }.toList() + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/Junit4LogConfigurator.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/Junit4LogConfigurator.kt new file mode 100644 index 000000000..f8217c196 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/Junit4LogConfigurator.kt @@ -0,0 +1,39 @@ +package com.malinskiy.marathon.vendor.junit4.configuration + +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.LoggerContext +import ch.qos.logback.classic.PatternLayout +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.ConsoleAppender +import ch.qos.logback.core.encoder.LayoutWrappingEncoder +import com.malinskiy.marathon.log.MarathonLogConfigurator +import com.malinskiy.marathon.vendor.VendorConfiguration +import org.slf4j.LoggerFactory + +class Junit4LogConfigurator : MarathonLogConfigurator { + override fun configure(vendorConfiguration: VendorConfiguration) { + val loggerContext = LoggerFactory.getILoggerFactory() as? LoggerContext ?: return + + val layout = PatternLayout() + layout.pattern = "%highlight(%.-1level %d{HH:mm:ss.SSS} [%thread] <%logger{40}> %msg%n)" + layout.context = loggerContext + layout.start(); + + val encoder = LayoutWrappingEncoder() + encoder.context = loggerContext + encoder.layout = layout + + val consoleAppender = ConsoleAppender() + consoleAppender.isWithJansi = true + consoleAppender.context = loggerContext + consoleAppender.name = "android-custom-console-appender" + consoleAppender.encoder = encoder + consoleAppender.start() + + val rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME) + + // replace the default appenders + rootLogger.detachAndStopAllAppenders() + rootLogger.addAppender(consoleAppender) + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/executor/DockerExecutorConfiguration.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/executor/DockerExecutorConfiguration.kt new file mode 100644 index 000000000..a62efb9e6 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/executor/DockerExecutorConfiguration.kt @@ -0,0 +1,3 @@ +package com.malinskiy.marathon.vendor.junit4.configuration.executor + +class DockerExecutorConfiguration : ExecutorConfigurationAdapter() diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/executor/ExecutorConfiguration.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/executor/ExecutorConfiguration.kt new file mode 100644 index 000000000..d25b10bce --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/executor/ExecutorConfiguration.kt @@ -0,0 +1,13 @@ +package com.malinskiy.marathon.vendor.junit4.configuration.executor + +import com.malinskiy.marathon.vendor.junit4.executor.ExecutionMode +import java.io.File + +interface ExecutorConfiguration { + val parallelism: Int + val javaHome: File? + val javaOptions: List + val useArgfiles: Boolean + val debug: Boolean + val mode: ExecutionMode +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/executor/ExecutorConfigurationAdapter.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/executor/ExecutorConfigurationAdapter.kt new file mode 100644 index 000000000..f4fa7eb1a --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/executor/ExecutorConfigurationAdapter.kt @@ -0,0 +1,13 @@ +package com.malinskiy.marathon.vendor.junit4.configuration.executor + +import com.malinskiy.marathon.vendor.junit4.executor.ExecutionMode +import java.io.File + +abstract class ExecutorConfigurationAdapter : ExecutorConfiguration { + override val parallelism: Int = Runtime.getRuntime().availableProcessors() + override val javaHome: File? = null + override val javaOptions: List = emptyList() + override val useArgfiles: Boolean = false + override val debug: Boolean = false + override val mode: ExecutionMode = ExecutionMode.ISOLATED +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/executor/KubernetesExecutorConfiguration.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/executor/KubernetesExecutorConfiguration.kt new file mode 100644 index 000000000..a32a93175 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/executor/KubernetesExecutorConfiguration.kt @@ -0,0 +1,3 @@ +package com.malinskiy.marathon.vendor.junit4.configuration.executor + +class KubernetesExecutorConfiguration : ExecutorConfigurationAdapter() diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/executor/LocalExecutorConfiguration.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/executor/LocalExecutorConfiguration.kt new file mode 100644 index 000000000..86a9ba146 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/configuration/executor/LocalExecutorConfiguration.kt @@ -0,0 +1,13 @@ +package com.malinskiy.marathon.vendor.junit4.configuration.executor + +import com.malinskiy.marathon.vendor.junit4.executor.ExecutionMode +import java.io.File + +class LocalExecutorConfiguration( + override val parallelism: Int = Runtime.getRuntime().availableProcessors(), + override val javaHome: File? = null, + override val javaOptions: List = emptyList(), + override val useArgfiles: Boolean = false, + override val debug: Boolean = false, + override val mode: ExecutionMode = ExecutionMode.ISOLATED +) : ExecutorConfigurationAdapter() diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/Booter.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/Booter.kt new file mode 100644 index 000000000..eb6edb4cd --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/Booter.kt @@ -0,0 +1,17 @@ +package com.malinskiy.marathon.vendor.junit4.executor + +import com.malinskiy.marathon.vendor.junit4.client.TestDiscoveryClient +import com.malinskiy.marathon.vendor.junit4.client.TestExecutorClient +import com.malinskiy.marathon.vendor.junit4.executor.listener.LineListener + +interface Booter { + val testExecutorClient: TestExecutorClient? + val testDiscoveryClient: TestDiscoveryClient? + + fun prepare() + fun recreate() + fun dispose() + + fun addLogListener(listener: LineListener) + fun removeLogListener(listener: LineListener) +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/ExecutionMode.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/ExecutionMode.kt new file mode 100644 index 000000000..2e7a0ecef --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/ExecutionMode.kt @@ -0,0 +1,6 @@ +package com.malinskiy.marathon.vendor.junit4.executor + +enum class ExecutionMode { + INPROCESS, + ISOLATED, +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/AbstractTestRunResultListener.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/AbstractTestRunResultListener.kt new file mode 100644 index 000000000..e4d7e3b51 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/AbstractTestRunResultListener.kt @@ -0,0 +1,49 @@ +package com.malinskiy.marathon.vendor.junit4.executor.listener + +import com.malinskiy.marathon.vendor.junit4.model.TestIdentifier +import com.malinskiy.marathon.vendor.junit4.model.TestRunResultsAccumulator + +abstract class AbstractTestRunResultListener : NoOpTestRunListener() { + private val runResult = TestRunResultsAccumulator() + + override suspend fun testRunStarted(runName: String, testCount: Int) { + runResult.testRunStarted(runName, testCount) + } + + override suspend fun testStarted(test: TestIdentifier) { + runResult.testStarted(test) + } + + override suspend fun testFailed(test: TestIdentifier, trace: String) { + runResult.testFailed(test, trace) + } + + override suspend fun testAssumptionFailure(test: TestIdentifier, trace: String) { + runResult.testAssumptionFailure(test, trace) + } + + override suspend fun testIgnored(test: TestIdentifier) { + runResult.testIgnored(test) + } + + override suspend fun testEnded(test: TestIdentifier, testMetrics: Map) { + runResult.testEnded(test, testMetrics) + } + + override suspend fun testRunFailed(errorMessage: String) { + runResult.testRunFailed(errorMessage) + handleTestRunResults(runResult) + } + + override suspend fun testRunStopped(elapsedTime: Long) { + runResult.testRunStopped(elapsedTime) + handleTestRunResults(runResult) + } + + override suspend fun testRunEnded(elapsedTime: Long, runMetrics: Map) { + runResult.testRunEnded(elapsedTime, runMetrics) + handleTestRunResults(runResult) + } + + abstract suspend fun handleTestRunResults(runResult: TestRunResultsAccumulator) +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/CompositeTestRunListener.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/CompositeTestRunListener.kt new file mode 100644 index 000000000..953d1e848 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/CompositeTestRunListener.kt @@ -0,0 +1,46 @@ +package com.malinskiy.marathon.vendor.junit4.executor.listener + +import com.malinskiy.marathon.vendor.junit4.model.TestIdentifier + + +class CompositeTestRunListener(private val listeners: List) : JUnit4TestRunListener { + private inline fun execute(f: (JUnit4TestRunListener) -> Unit) { + listeners.forEach(f) + } + + override suspend fun testRunStarted(runName: String, testCount: Int) { + execute { it.testRunStarted(runName, testCount) } + } + + override suspend fun testStarted(test: TestIdentifier) { + execute { it.testStarted(test) } + } + + override suspend fun testAssumptionFailure(test: TestIdentifier, trace: String) { + execute { it.testAssumptionFailure(test, trace) } + } + + override suspend fun testRunStopped(elapsedTime: Long) { + execute { it.testRunStopped(elapsedTime) } + } + + override suspend fun testFailed(test: TestIdentifier, trace: String) { + execute { it.testFailed(test, trace) } + } + + override suspend fun testEnded(test: TestIdentifier, testMetrics: Map) { + execute { it.testEnded(test, testMetrics) } + } + + override suspend fun testIgnored(test: TestIdentifier) { + execute { it.testIgnored(test) } + } + + override suspend fun testRunFailed(errorMessage: String) { + execute { it.testRunFailed(errorMessage) } + } + + override suspend fun testRunEnded(elapsedTime: Long, runMetrics: Map) { + execute { it.testRunEnded(elapsedTime, runMetrics) } + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/DebugTestRunListener.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/DebugTestRunListener.kt new file mode 100644 index 000000000..21bb4d193 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/DebugTestRunListener.kt @@ -0,0 +1,55 @@ +package com.malinskiy.marathon.vendor.junit4.executor.listener + +import com.malinskiy.marathon.log.MarathonLogging +import com.malinskiy.marathon.test.Test +import com.malinskiy.marathon.test.toTestName +import com.malinskiy.marathon.vendor.junit4.Junit4Device +import com.malinskiy.marathon.vendor.junit4.model.TestIdentifier + +class DebugTestRunListener(private val device: Junit4Device, private val expectedTests: List) : JUnit4TestRunListener { + + private val logger = MarathonLogging.logger("DebugTestRunListener") + + override suspend fun testRunStarted(runName: String, testCount: Int) { + logger.info { "testRunStarted ${device.serialNumber}" } + } + + override suspend fun testStarted(test: TestIdentifier) { + logger.info { "testStarted ${device.serialNumber} test = $test" } + if (!expectedTests.contains(test.toTest())) { + logger.error { + "Unexpected test ${ + test.toTest().toTestName() + }. Expected one of: ${expectedTests.joinToString(separator = "\n") { it.toTestName() }}" + } + } + } + + override suspend fun testAssumptionFailure(test: TestIdentifier, trace: String) { + logger.info { "testAssumptionFailure ${device.serialNumber} test = $test trace = $trace" } + } + + override suspend fun testRunStopped(elapsedTime: Long) { + logger.info { "testRunStopped ${device.serialNumber} elapsedTime = $elapsedTime" } + } + + override suspend fun testFailed(test: TestIdentifier, trace: String) { + logger.info { "testFailed ${device.serialNumber} test = $test trace = $trace" } + } + + override suspend fun testEnded(test: TestIdentifier, testMetrics: Map) { + logger.info { "testEnded ${device.serialNumber} test = $test" } + } + + override suspend fun testIgnored(test: TestIdentifier) { + logger.info { "testIgnored ${device.serialNumber} test = $test" } + } + + override suspend fun testRunFailed(errorMessage: String) { + logger.info { "testRunFailed ${device.serialNumber} errorMessage = $errorMessage" } + } + + override suspend fun testRunEnded(elapsedTime: Long, runMetrics: Map) { + logger.info { "testRunEnded elapsedTime $elapsedTime" } + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/DeviceLogListener.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/DeviceLogListener.kt new file mode 100644 index 000000000..8aa7aaa59 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/DeviceLogListener.kt @@ -0,0 +1,16 @@ +package com.malinskiy.marathon.vendor.junit4.executor.listener + +import com.malinskiy.marathon.device.DevicePoolId +import com.malinskiy.marathon.device.toDeviceInfo +import com.malinskiy.marathon.report.logs.LogWriter +import com.malinskiy.marathon.vendor.junit4.Junit4Device + +class DeviceLogListener( + private val device: Junit4Device, + private val devicePoolId: DevicePoolId, + private val logWriter: LogWriter, +) : LineListener { + override fun onLine(line: String) { + logWriter.appendLogs(devicePoolId, device.toDeviceInfo(), line) + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/JUnit4TestRunListener.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/JUnit4TestRunListener.kt new file mode 100644 index 000000000..6846f3311 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/JUnit4TestRunListener.kt @@ -0,0 +1,23 @@ +package com.malinskiy.marathon.vendor.junit4.executor.listener + +import com.malinskiy.marathon.vendor.junit4.model.TestIdentifier + +interface JUnit4TestRunListener { + suspend fun testRunStarted(runName: String, testCount: Int) {} + + suspend fun testStarted(test: TestIdentifier) {} + + suspend fun testFailed(test: TestIdentifier, trace: String) {} + + suspend fun testAssumptionFailure(test: TestIdentifier, trace: String) {} + + suspend fun testIgnored(test: TestIdentifier) {} + + suspend fun testEnded(test: TestIdentifier, testMetrics: Map) {} + + suspend fun testRunFailed(errorMessage: String) {} + + suspend fun testRunStopped(elapsedTime: Long) {} + + suspend fun testRunEnded(elapsedTime: Long, runMetrics: Map) {} +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/LineListener.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/LineListener.kt new file mode 100644 index 000000000..93d833053 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/LineListener.kt @@ -0,0 +1,5 @@ +package com.malinskiy.marathon.vendor.junit4.executor.listener + +interface LineListener { + fun onLine(line: String) +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/LogListener.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/LogListener.kt new file mode 100644 index 000000000..9175a6cce --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/LogListener.kt @@ -0,0 +1,57 @@ +package com.malinskiy.marathon.vendor.junit4.executor.listener + +import com.malinskiy.marathon.device.DevicePoolId +import com.malinskiy.marathon.device.toDeviceInfo +import com.malinskiy.marathon.execution.Attachment +import com.malinskiy.marathon.execution.AttachmentType +import com.malinskiy.marathon.report.attachment.AttachmentListener +import com.malinskiy.marathon.report.attachment.AttachmentProvider +import com.malinskiy.marathon.report.logs.LogWriter +import com.malinskiy.marathon.vendor.junit4.Junit4Device +import com.malinskiy.marathon.vendor.junit4.model.TestIdentifier + +class LogListener(private val device: Junit4Device, + private val devicePoolId: DevicePoolId, + private val testBatchId: String, + private val logWriter: LogWriter +) : NoOpTestRunListener(), AttachmentProvider, LineListener { + private val attachmentListeners = mutableListOf() + + override fun registerListener(listener: AttachmentListener) { + attachmentListeners.add(listener) + } + + private val stringBuffer = StringBuffer(4096) + + override fun onLine(line: String) { + stringBuffer.appendLine(line) + } + + override suspend fun testRunStarted(runName: String, testCount: Int) { + device.addLogListener(this) + } + + override suspend fun testStarted(test: TestIdentifier) { + super.testStarted(test) + stringBuffer.reset() + } + + override suspend fun testEnded(test: TestIdentifier, testMetrics: Map) { + if (stringBuffer.isNotEmpty()) { + val file = logWriter.saveLogs(test.toTest(), devicePoolId, testBatchId, device.toDeviceInfo(), listOf(stringBuffer.toString())) + attachmentListeners.forEach { it.onAttachment(test.toTest(), Attachment(file, AttachmentType.LOG)) } + } + } + + override suspend fun testRunEnded(elapsedTime: Long, runMetrics: Map) { + device.removeLogListener(this) + } + + override suspend fun testRunFailed(errorMessage: String) { + device.removeLogListener(this) + } + + private fun StringBuffer.reset() { + delete(0, length) + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/NoOpTestRunListener.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/NoOpTestRunListener.kt new file mode 100644 index 000000000..91b51bc20 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/NoOpTestRunListener.kt @@ -0,0 +1,23 @@ +package com.malinskiy.marathon.vendor.junit4.executor.listener + +import com.malinskiy.marathon.vendor.junit4.model.TestIdentifier + +open class NoOpTestRunListener : JUnit4TestRunListener { + override suspend fun testRunStarted(runName: String, testCount: Int) {} + + override suspend fun testStarted(test: TestIdentifier) {} + + override suspend fun testFailed(test: TestIdentifier, trace: String) {} + + override suspend fun testAssumptionFailure(test: TestIdentifier, trace: String) {} + + override suspend fun testIgnored(test: TestIdentifier) {} + + override suspend fun testEnded(test: TestIdentifier, testMetrics: Map) {} + + override suspend fun testRunFailed(errorMessage: String) {} + + override suspend fun testRunStopped(elapsedTime: Long) {} + + override suspend fun testRunEnded(elapsedTime: Long, runMetrics: Map) {} +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/ProgressTestRunListener.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/ProgressTestRunListener.kt new file mode 100644 index 000000000..1ee83fed3 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/ProgressTestRunListener.kt @@ -0,0 +1,44 @@ +package com.malinskiy.marathon.vendor.junit4.executor.listener + +import com.malinskiy.marathon.device.Device +import com.malinskiy.marathon.device.DevicePoolId +import com.malinskiy.marathon.device.toDeviceInfo +import com.malinskiy.marathon.execution.progress.ProgressReporter +import com.malinskiy.marathon.vendor.junit4.model.TestIdentifier + +class ProgressTestRunListener( + private val device: Device, + private val poolId: DevicePoolId, + private val progressTracker: ProgressReporter +) : NoOpTestRunListener() { + + private val failed = mutableMapOf() + private val ignored = mutableMapOf() + + override suspend fun testStarted(test: TestIdentifier) { + failed[test] = false + ignored[test] = false + progressTracker.testStarted(poolId, device.toDeviceInfo(), test.toTest()) + } + + override suspend fun testFailed(test: TestIdentifier, trace: String) { + failed[test] = true + } + + override suspend fun testAssumptionFailure(test: TestIdentifier, trace: String) { + testIgnored(test) + } + + override suspend fun testEnded(test: TestIdentifier, testMetrics: Map) { + if (failed[test] == true) { + progressTracker.testFailed(poolId, device.toDeviceInfo(), test.toTest()) + } else if (ignored[test] == false) { + progressTracker.testPassed(poolId, device.toDeviceInfo(), test.toTest()) + } + } + + override suspend fun testIgnored(test: TestIdentifier) { + ignored[test] = true + progressTracker.testIgnored(poolId, device.toDeviceInfo(), test.toTest()) + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/TestRunResultsListener.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/TestRunResultsListener.kt new file mode 100644 index 000000000..cd529ef7c --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/listener/TestRunResultsListener.kt @@ -0,0 +1,152 @@ +package com.malinskiy.marathon.vendor.junit4.executor.listener + +import com.malinskiy.marathon.device.Device +import com.malinskiy.marathon.device.toDeviceInfo +import com.malinskiy.marathon.execution.Attachment +import com.malinskiy.marathon.execution.TestBatchResults +import com.malinskiy.marathon.execution.TestResult +import com.malinskiy.marathon.execution.TestStatus +import com.malinskiy.marathon.log.MarathonLogging +import com.malinskiy.marathon.report.attachment.AttachmentListener +import com.malinskiy.marathon.report.attachment.AttachmentProvider +import com.malinskiy.marathon.test.Test +import com.malinskiy.marathon.test.TestBatch +import com.malinskiy.marathon.test.toTestName +import com.malinskiy.marathon.time.Timer +import com.malinskiy.marathon.vendor.junit4.model.JUnit4TestResult +import com.malinskiy.marathon.vendor.junit4.model.JUnit4TestStatus +import com.malinskiy.marathon.vendor.junit4.model.TestIdentifier +import com.malinskiy.marathon.vendor.junit4.model.TestRunResultsAccumulator +import kotlinx.coroutines.CompletableDeferred + +class TestRunResultsListener( + private val testBatch: TestBatch, + private val device: Device, + private val deferred: CompletableDeferred, + private val timer: Timer, + attachmentProviders: List +) : AbstractTestRunResultListener(), AttachmentListener { + + private val attachments: MutableMap> = mutableMapOf() + private val creationTime = timer.currentTimeMillis() + + init { + attachmentProviders.forEach { + it.registerListener(this) + } + } + + override fun onAttachment(test: Test, attachment: Attachment) { + val list = attachments[test] + if (list == null) { + attachments[test] = mutableListOf() + } + + attachments[test]!!.add(attachment) + } + + private val logger = MarathonLogging.logger("TestRunResultsListener") + + override suspend fun handleTestRunResults(runResult: TestRunResultsAccumulator) { + val results = runResult.testResults + val tests = testBatch.tests.associateBy { it.identifier() } + + val testResults = results.map { + it.toTestResult(device) + } + + val nonNullTestResults = testResults.filter { + it.test.method != "null" + } + + val finished = nonNullTestResults.filter { + results[it.test.identifier()]?.isSuccessful() ?: false + } + + val (reportedIncompleteTests, reportedNonNullTests) = nonNullTestResults.partition { it.status == TestStatus.INCOMPLETE } + + val failed = reportedNonNullTests.filterNot { + val status = results[it.test.identifier()] + when { + status?.isSuccessful() == true -> true + else -> false + } + } + + val uncompleted = reportedIncompleteTests + tests + .filterNot { expectedTest -> + results.containsKey(expectedTest.key) + } + .values + .createUncompletedTestResults(runResult, device) + + if (uncompleted.isNotEmpty()) { + uncompleted.forEach { + logger.warn { "uncompleted = ${it.test.toTestName()}, ${device.serialNumber}" } + } + } + + deferred.complete(TestBatchResults(device, finished, failed, uncompleted)) + } + + private fun Collection.createUncompletedTestResults( + testRunResult: TestRunResultsAccumulator, + device: Device + ): Collection { + + val lastCompletedTestEndTime = testRunResult + .testResults + .values + .maxBy { it.endTime } + ?.endTime + ?: creationTime + + return map { + TestResult( + it, + device.toDeviceInfo(), + testBatch.id, + TestStatus.INCOMPLETE, + lastCompletedTestEndTime, + timer.currentTimeMillis(), + testRunResult.runFailureMessage, + ) + } + } + + private fun Map.Entry.toTestResult(device: Device): TestResult { + val testInstanceFromBatch = testBatch.tests.find { "${it.pkg}.${it.clazz}" == key.className && it.method == key.testName } + val test = key.toTest() + val attachments = attachments[test] ?: emptyList() + return TestResult( + test = testInstanceFromBatch ?: test, + device = device.toDeviceInfo(), + status = value.status.toMarathonStatus(), + startTime = value.startTime, + endTime = value.endTime, + stacktrace = value.stackTrace, + attachments = attachments, + testBatchId = testBatch.id, + ) + } + + private fun Test.identifier(): TestIdentifier { + return TestIdentifier("$pkg.$clazz", method) + } + + private fun JUnit4TestResult.isSuccessful(): Boolean = + when (status) { + JUnit4TestStatus.PASSED, JUnit4TestStatus.IGNORED, JUnit4TestStatus.ASSUMPTION_FAILURE -> true + else -> false + } +} + +private operator fun JUnit4TestStatus.plus(value: JUnit4TestStatus): JUnit4TestStatus { + return when (this) { + JUnit4TestStatus.FAILURE -> JUnit4TestStatus.FAILURE + JUnit4TestStatus.PASSED -> value + JUnit4TestStatus.IGNORED -> JUnit4TestStatus.IGNORED + JUnit4TestStatus.INCOMPLETE -> JUnit4TestStatus.INCOMPLETE + JUnit4TestStatus.ASSUMPTION_FAILURE -> JUnit4TestStatus.ASSUMPTION_FAILURE + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/local/LocalhostBooter.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/local/LocalhostBooter.kt new file mode 100644 index 000000000..17c5319ea --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/executor/local/LocalhostBooter.kt @@ -0,0 +1,143 @@ +package com.malinskiy.marathon.vendor.junit4.executor.local + +import com.malinskiy.marathon.vendor.junit4.booter.Mode +import com.malinskiy.marathon.vendor.junit4.client.TestDiscoveryClient +import com.malinskiy.marathon.vendor.junit4.client.TestExecutorClient +import com.malinskiy.marathon.vendor.junit4.configuration.Junit4Configuration +import com.malinskiy.marathon.vendor.junit4.configuration.executor.ExecutorConfigurationAdapter +import com.malinskiy.marathon.vendor.junit4.executor.Booter +import com.malinskiy.marathon.vendor.junit4.executor.ExecutionMode +import com.malinskiy.marathon.vendor.junit4.executor.listener.LineListener +import io.grpc.ManagedChannelBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor +import java.io.File +import java.io.InputStream +import java.nio.file.Path +import java.nio.file.Paths +import java.util.Scanner +import java.util.concurrent.ConcurrentLinkedQueue + + +class LocalhostBooter( + private val conf: Junit4Configuration, + private val controlPort: Int = 50051, + private val mode: Mode = Mode.RUNNER, + private val debug: Boolean? = null, +) : Booter { + private val logListeners = ConcurrentLinkedQueue() + + private var useArgfiles: Boolean = true + private lateinit var process: Process + private lateinit var args: String + private lateinit var argsFile: File + private lateinit var javaBinary: Path + override var testExecutorClient: TestExecutorClient? = null + override var testDiscoveryClient: TestDiscoveryClient? = null + + override fun prepare() { + val executorConfiguration = conf.executorConfiguration as ExecutorConfigurationAdapter + + javaBinary = executorConfiguration.javaHome?.let { Paths.get(it.absolutePath, "bin", "java") } ?: Paths.get("java") + val classpath = "$booterJar" + + args = StringBuilder().apply { + if (debug ?: conf.debugBooter) { + append("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=1044 ") + } + when (mode) { + Mode.RUNNER -> { + if (conf.executorConfiguration.mode == ExecutionMode.INPROCESS) { + append(executorConfiguration.javaOptions.joinToString(separator = "") { "$it " }) + } + } + Mode.DISCOVER -> { + } + } + append("-cp ") + append(classpath) + append(" com.malinskiy.marathon.vendor.junit4.booter.BooterKt") + }.toString() + + useArgfiles = executorConfiguration.useArgfiles + if (useArgfiles) { + argsFile = File("argsfile-${controlPort}").apply { + deleteOnExit() + delete() + appendText(args) + } + } + + fork(false) + } + + override fun recreate() { + fork(true) + } + + override fun addLogListener(logListener: LineListener) { + logListeners.add(logListener) + } + + override fun removeLogListener(logListener: LineListener) { + logListeners.remove(logListener) + } + + override fun dispose() { + testExecutorClient?.close() + testDiscoveryClient?.close() + process.destroy() + process.waitFor() + } + + private fun fork(clean: Boolean) { + if (clean) { + dispose() + } + + process = if (useArgfiles) { + ProcessBuilder(javaBinary.toString(), "@${argsFile.absolutePath}") + } else { + ProcessBuilder(javaBinary.toString(), *args.split(" ").toTypedArray()) + }.apply { + environment()["PORT"] = controlPort.toString() + environment()["MODE"] = mode.toString() + environment()["EXEC_MODE"] = conf.executorConfiguration.mode.toString() + }.start() + + + inheritIO(process.inputStream) + inheritIO(process.errorStream) + + val localChannel = ManagedChannelBuilder.forAddress("localhost", controlPort).apply { + usePlaintext() + executor(Dispatchers.IO.asExecutor()) + }.build() + + when (mode) { + Mode.RUNNER -> testExecutorClient = TestExecutorClient(localChannel, conf.executorConfiguration) + Mode.DISCOVER -> testDiscoveryClient = TestDiscoveryClient(localChannel) + } + } + + private fun inheritIO(src: InputStream) { + Thread { + val scanner = Scanner(src) + while (scanner.hasNextLine()) { + val line = scanner.nextLine() + logListeners.forEach { listener -> + listener.onLine(line) + } + } + }.start() + } + + companion object { + val booterJar: File by lazy { + val tempFile = File.createTempFile("marathon", "booter.jar") + javaClass.getResourceAsStream("/vendor-junit4-booter-all.jar").copyTo(tempFile.outputStream()) + tempFile.deleteOnExit() + tempFile + } + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/extensions/ClassLoaderExtensions.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/extensions/ClassLoaderExtensions.kt new file mode 100644 index 000000000..26ff2da05 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/extensions/ClassLoaderExtensions.kt @@ -0,0 +1,14 @@ +package com.malinskiy.marathon.vendor.junit4.extensions + +inline fun ClassLoader.switch(block: () -> T): T { + val originalClassLoader = Thread.currentThread().contextClassLoader + return try { + Thread.currentThread().contextClassLoader = this + block.invoke() + } catch (e: Exception) { + e.printStackTrace() + throw e + } finally { + Thread.currentThread().contextClassLoader = originalClassLoader + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/extensions/TestExtensions.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/extensions/TestExtensions.kt new file mode 100644 index 000000000..e0ca2abd5 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/extensions/TestExtensions.kt @@ -0,0 +1,8 @@ +package com.malinskiy.marathon.vendor.junit4.extensions + +import com.malinskiy.marathon.test.Test + +const val JUNIT_IGNORE_META_PROPERTY_NAME = "org.junit.Ignore" +private val ignoredMetaProperties = setOf(JUNIT_IGNORE_META_PROPERTY_NAME) + +fun Test.isIgnored() = metaProperties.map { it.name }.intersect(ignoredMetaProperties).isNotEmpty() diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/install/Junit4AppInstaller.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/install/Junit4AppInstaller.kt new file mode 100644 index 000000000..7b3c125d6 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/install/Junit4AppInstaller.kt @@ -0,0 +1,11 @@ +package com.malinskiy.marathon.vendor.junit4.install + +import com.malinskiy.marathon.vendor.junit4.configuration.Junit4Configuration + +class Junit4AppInstaller(conf: Junit4Configuration) { + /** + * Transfer local classpath to remote device if device is not local + */ + suspend fun install() { + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/model/JUnit4TestBundle.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/model/JUnit4TestBundle.kt new file mode 100644 index 000000000..c00a65431 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/model/JUnit4TestBundle.kt @@ -0,0 +1,11 @@ +package com.malinskiy.marathon.vendor.junit4.model + +import com.malinskiy.marathon.execution.bundle.TestBundle +import java.io.File + +class JUnit4TestBundle( + override val id: String, + val applicationClasspath: List? = null, + val testClasspath: List? = null, + val workdir: String? = null, +) : TestBundle() diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/model/JUnit4TestResult.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/model/JUnit4TestResult.kt new file mode 100644 index 000000000..20818afd4 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/model/JUnit4TestResult.kt @@ -0,0 +1,9 @@ +package com.malinskiy.marathon.vendor.junit4.model + +data class JUnit4TestResult( + var status: JUnit4TestStatus = JUnit4TestStatus.INCOMPLETE, + var startTime: Long = System.currentTimeMillis(), + var endTime: Long = 0, + var stackTrace: String? = null, + var metrics: Map? = null +) diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/model/JUnit4TestStatus.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/model/JUnit4TestStatus.kt new file mode 100644 index 000000000..11474dae2 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/model/JUnit4TestStatus.kt @@ -0,0 +1,28 @@ +package com.malinskiy.marathon.vendor.junit4.model + +import com.malinskiy.marathon.execution.TestStatus + +enum class JUnit4TestStatus { + /** Test failed. */ + FAILURE, + + /** Test passed */ + PASSED, + + /** Test started but not ended */ + INCOMPLETE, + + /** Test assumption failure */ + ASSUMPTION_FAILURE, + + /** Test ignored */ + IGNORED; + + fun toMarathonStatus(): TestStatus = when (this) { + PASSED -> TestStatus.PASSED + FAILURE -> TestStatus.FAILURE + IGNORED -> TestStatus.IGNORED + INCOMPLETE -> TestStatus.INCOMPLETE + ASSUMPTION_FAILURE -> TestStatus.ASSUMPTION_FAILURE + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/model/TestIdentifier.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/model/TestIdentifier.kt new file mode 100644 index 000000000..17e7bffca --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/model/TestIdentifier.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2019 Anton Malinskiy + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.malinskiy.marathon.vendor.junit4.model + +import com.malinskiy.marathon.test.Test + +data class TestIdentifier( + val className: String, + val testName: String +) { + fun toTest(): Test { + val pkg = className.substringBeforeLast(".") + val className = className.substringAfterLast(".") + val methodName = testName + return Test(pkg, className, methodName, emptyList()) + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/model/TestRunResultsAccumulator.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/model/TestRunResultsAccumulator.kt new file mode 100644 index 000000000..4b51d9aa7 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/model/TestRunResultsAccumulator.kt @@ -0,0 +1,203 @@ +package com.malinskiy.marathon.vendor.junit4.model + +import com.malinskiy.marathon.log.MarathonLogging + +/** + * Holds results from a single test run. + * + * + * Maintains an accurate count of tests, and tracks incomplete tests. + * + * + * Not thread safe! The test* callbacks must be called in order + */ +class TestRunResultsAccumulator { + + val logger = MarathonLogging.logger { } + + var name: String = "not started" + private set + val testResults = LinkedHashMap() + private val runMetrics = HashMap() + var isRunComplete = false + var isCountDirty = false + var elapsedTime: Long = 0 + private set + + private var statusCounts: Map = mutableMapOf() + + /** + * Return the run failure error message, `null` if run did not fail. + */ + var runFailureMessage: String? = null + private set + + var aggregateMetrics = false + + val completedTests: Set + get() { + val completedTests = LinkedHashSet() + for ((key, value) in testResults) { + if (value.status != JUnit4TestStatus.INCOMPLETE) { + completedTests.add(key) + } + } + return completedTests + } + + val isRunFailure: Boolean + get() = runFailureMessage != null + + val numTests: Int + get() = testResults.size + + val numCompleteTests: Int + get() = numTests - getNumTestsInState(JUnit4TestStatus.INCOMPLETE) + + /** + * Return total number of tests in a failure state (failed, assumption failure) + */ + val numAllFailedTests: Int + get() = getNumTestsInState(JUnit4TestStatus.FAILURE) + getNumTestsInState(JUnit4TestStatus.ASSUMPTION_FAILURE) + + /** + * Gets the number of tests in given state for this run. + */ + fun getNumTestsInState(status: JUnit4TestStatus): Int { + if (isCountDirty) { + statusCounts = testResults.values.groupingBy { it.status }.eachCount() + } + + return statusCounts[status] ?: 0 + } + + /** + * @return `true` if test run had any failed or error tests. + */ + fun hasFailedTests(): Boolean { + return numAllFailedTests > 0 + } + + + fun testRunStarted(runName: String, testCount: Int) { + name = runName + isRunComplete = false + runFailureMessage = null + } + + fun testStarted(test: TestIdentifier) { + testStarted(test, System.currentTimeMillis()) + } + + fun testStarted(test: TestIdentifier, startTime: Long) { + val res = JUnit4TestResult() + res.startTime = startTime + addTestResult(test, res) + } + + private fun addTestResult(test: TestIdentifier, testResult: JUnit4TestResult) { + isCountDirty = true + testResults[test] = testResult + } + + private fun updateTestResult(test: TestIdentifier, status: JUnit4TestStatus, trace: String?) { + var r: JUnit4TestResult? = testResults[test] + if (r == null) { + logger.debug { "received test event without test start for ${test.className}#${test.testName}" } + r = JUnit4TestResult() + } + r.status = status + r.stackTrace = trace + addTestResult(test, r) + } + + fun testFailed(test: TestIdentifier, trace: String) { + updateTestResult(test, JUnit4TestStatus.FAILURE, trace) + } + + fun testAssumptionFailure(test: TestIdentifier, trace: String) { + updateTestResult(test, JUnit4TestStatus.ASSUMPTION_FAILURE, trace) + } + + fun testIgnored(test: TestIdentifier) { + updateTestResult(test, JUnit4TestStatus.IGNORED, null) + } + + fun testEnded(test: TestIdentifier, testMetrics: Map) { + testEnded(test, System.currentTimeMillis(), testMetrics) + } + + fun testEnded(test: TestIdentifier, endTime: Long, testMetrics: Map) { + var result: JUnit4TestResult? = testResults[test] + if (result == null) { + result = JUnit4TestResult() + } + if (result.status == JUnit4TestStatus.INCOMPLETE) { + result.status = JUnit4TestStatus.PASSED + } + result.endTime = endTime + result.metrics = testMetrics + addTestResult(test, result) + } + + fun testRunFailed(errorMessage: String) { + runFailureMessage = errorMessage + fillEndTime() + } + + private fun fillEndTime() { + testResults.values.filter { it.endTime == 0L }.forEach { it -> + it.endTime = System.currentTimeMillis() + } + } + + fun testRunStopped(elapsedTime: Long) { + this.elapsedTime += elapsedTime + isRunComplete = true + fillEndTime() + } + + fun testRunEnded(elapsedTime: Long, runMetrics: Map) { + if (aggregateMetrics) { + for ((key, value) in runMetrics) { + combineValues(runMetrics[key], value)?.let { + this.runMetrics[key] = it + } + } + } else { + this.runMetrics.putAll(runMetrics) + } + this.elapsedTime += elapsedTime + isRunComplete = true + fillEndTime() + } + + /** + * Combine old and new metrics value + * + * @param existingValue + * @param newValue + * @return the combination of the two string as Long or Double value. + */ + private fun combineValues(existingValue: String?, newValue: String): String? { + if (existingValue != null) { + try { + val existingLong = java.lang.Long.parseLong(existingValue) + val newLong = java.lang.Long.parseLong(newValue) + return java.lang.Long.toString(existingLong + newLong) + } catch (e: NumberFormatException) { + // not a long, skip to next + } + + try { + val existingDouble = java.lang.Double.parseDouble(existingValue) + val newDouble = java.lang.Double.parseDouble(newValue) + return java.lang.Double.toString(existingDouble + newDouble) + } catch (e: NumberFormatException) { + // not a double either, fall through + } + } + // default to overriding existingValue + return newValue + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/parsing/JupiterTestParser.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/parsing/JupiterTestParser.kt new file mode 100644 index 000000000..0d55c43bc --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/parsing/JupiterTestParser.kt @@ -0,0 +1,166 @@ +package com.malinskiy.marathon.vendor.junit4.parsing + +import com.malinskiy.marathon.execution.Configuration +import com.malinskiy.marathon.execution.TestParser +import com.malinskiy.marathon.log.MarathonLogging +import com.malinskiy.marathon.test.MetaProperty +import com.malinskiy.marathon.test.Test +import com.malinskiy.marathon.vendor.junit4.Junit4TestBundleIdentifier +import com.malinskiy.marathon.vendor.junit4.configuration.Junit4Configuration +import com.malinskiy.marathon.vendor.junit4.extensions.switch +import org.junit.platform.engine.discovery.ClassNameFilter +import org.junit.platform.engine.discovery.DiscoverySelectors +import org.junit.platform.engine.support.descriptor.ClassSource +import org.junit.platform.engine.support.descriptor.MethodSource +import org.junit.platform.launcher.TestIdentifier +import org.junit.platform.launcher.TestPlan +import org.junit.platform.launcher.core.LauncherConfig +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder +import org.junit.platform.launcher.core.LauncherFactory +import org.junit.platform.launcher.listeners.discovery.LauncherDiscoveryListeners +import org.junit.vintage.engine.VintageTestEngine +import java.io.File +import java.net.URLClassLoader +import kotlin.system.measureTimeMillis + +class JupiterTestParser(private val testBundleIdentifier: Junit4TestBundleIdentifier) : TestParser { + private val logger = MarathonLogging.logger {} + + override suspend fun extract(configuration: Configuration): List { + val discoveredTests = mutableListOf() + + measureTimeMillis { + val conf = configuration.vendorConfiguration as Junit4Configuration + + /** + * Parallelization is not supported currently by junit5 + */ + var counter = 1 + conf.testBundlesCompat().forEach { bundle -> + logger.info { "Parsing ${bundle.id}" } + println("Parsing ${counter++} of ${conf.testBundlesCompat().size}") + + val bundleTests = mutableListOf() + + val cp = mutableListOf().apply { + bundle.testClasspath?.let { addAll(it) } + bundle.applicationClasspath?.let { addAll(it) } + }.toList() + + val classpathToScan = cp.map { it.toURI().toURL() } + .toTypedArray() + val classloader: ClassLoader = URLClassLoader.newInstance( + classpathToScan, + Thread.currentThread().contextClassLoader + ) + + val plan: TestPlan = classloader.switch { + val launcherConfig = LauncherConfig.builder() + .addTestEngines(VintageTestEngine()) + .enableTestEngineAutoRegistration(false) + .build() + val launcher = LauncherFactory.create(launcherConfig) + + val discoveryRequest = LauncherDiscoveryRequestBuilder() + .selectors( + DiscoverySelectors.selectPackage(conf.testPackageRoot) + ) + .listeners(LauncherDiscoveryListeners.logging()) + .filters( + ClassNameFilter.includeClassNamePatterns(ClassNameFilter.STANDARD_INCLUDE_PATTERN) + ) + .build() + + launcher.discover(discoveryRequest) + } + + if (plan.containsTests()) { + plan.roots.forEach { root: TestIdentifier -> + val tests = plan.getChildren(root) + tests.forEach { test: TestIdentifier -> + when { + test.isContainer -> { + plan.getChildren(test).forEach { method -> + if (method.source.isPresent) { + val source = method.source.get() + when (source) { + is MethodSource -> { + bundleTests.add(source.toTest()) + } + is ClassSource -> { + val testIdentifier = plan.getTestIdentifier(method.uniqueId) + if (plan.getParent(testIdentifier).isPresent) { + val parent = plan.getParent(testIdentifier).get() + val classSource = parent.source.get() as ClassSource + bundleTests.add(classSource.toTest(source, testIdentifier.displayName)) + } else { + logger.warn { "Unknown test ${method.uniqueId}" } + } + } + else -> { + logger.warn { "Unknown test ${method.uniqueId}" } + } + } + } else if (method.isContainer) { + //Most likely a parameterized test + plan.getChildren(method).forEach { parameterizedTest -> + if (parameterizedTest.source.isPresent) { + val source = parameterizedTest.source.get() + when (source) { + is MethodSource -> { + bundleTests.add(source.toParameterizedTest(parameterizedTest)) + } + else -> { + logger.warn { "Unknown test ${parameterizedTest.uniqueId}" } + } + } + } else { + logger.warn { "Unknown test ${parameterizedTest.uniqueId}" } + } + } + } else { + logger.warn { "Unknown test ${method.uniqueId}" } + } + } + } + } + } + } + } + + bundleTests.forEach { + testBundleIdentifier.put(it, bundle) + } + discoveredTests.addAll(bundleTests) + } + }.let { + logger.debug { "Parsing finished in ${it}ms" } + } + + return discoveredTests.toList() + } +} + +private fun ClassSource.toTest(child: ClassSource, methodName: String): Test { + val clazz = className.substringAfterLast(".") + val pkg = className.substringBeforeLast(".") + return Test(pkg, clazz, methodName, emptyList()) +} + +private fun MethodSource.toTest(): Test { + val clazz = className.substringAfterLast(".") + val pkg = className.substringBeforeLast(".") + val meta = javaMethod.declaredAnnotations.mapNotNull { it.annotationClass.qualifiedName?.let { name -> MetaProperty(name) } } + + javaClass.declaredAnnotations.mapNotNull { it.annotationClass.qualifiedName?.let { name -> MetaProperty(name) } } + + return Test(pkg, clazz, methodName, meta) +} + +private fun MethodSource.toParameterizedTest(parameterizedTest: TestIdentifier): Test { + val clazz = className.substringAfterLast(".") + val pkg = className.substringBeforeLast(".") + val meta = javaMethod.declaredAnnotations.mapNotNull { it.annotationClass.qualifiedName?.let { name -> MetaProperty(name) } } + + javaClass.declaredAnnotations.mapNotNull { it.annotationClass.qualifiedName?.let { name -> MetaProperty(name) } } + + return Test(pkg, clazz, parameterizedTest.displayName, meta) +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/parsing/RemoteJupiterTestParser.kt b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/parsing/RemoteJupiterTestParser.kt new file mode 100644 index 000000000..ab8734d37 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/main/kotlin/com/malinskiy/marathon/vendor/junit4/parsing/RemoteJupiterTestParser.kt @@ -0,0 +1,77 @@ +package com.malinskiy.marathon.vendor.junit4.parsing + +import com.malinskiy.marathon.execution.Configuration +import com.malinskiy.marathon.execution.TestParser +import com.malinskiy.marathon.log.MarathonLogging +import com.malinskiy.marathon.test.Test +import com.malinskiy.marathon.vendor.junit4.Junit4TestBundleIdentifier +import com.malinskiy.marathon.vendor.junit4.booter.Mode +import com.malinskiy.marathon.vendor.junit4.configuration.Junit4Configuration +import com.malinskiy.marathon.vendor.junit4.configuration.executor.ExecutorConfigurationAdapter +import com.malinskiy.marathon.vendor.junit4.executor.local.LocalhostBooter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.util.concurrent.ConcurrentLinkedQueue +import kotlin.system.measureTimeMillis + +class RemoteJupiterTestParser(private val testBundleIdentifier: Junit4TestBundleIdentifier) : TestParser { + private val logger = MarathonLogging.logger {} + + private val controlPort = 49000 + + override suspend fun extract(configuration: Configuration): List { + val discoveredTests = mutableListOf() + + measureTimeMillis { + val conf = configuration.vendorConfiguration as Junit4Configuration + val executorConfiguration = conf.executorConfiguration as ExecutorConfigurationAdapter + var counter = 1 + val job = SupervisorJob() + + val workerPool = ConcurrentLinkedQueue() + for (i in 1..executorConfiguration.parallelism) { + workerPool.add(LocalhostBooter(conf, controlPort, Mode.DISCOVER, debug = false).apply { prepare() }) + } + + conf.testBundlesCompat() + .asSequence() + .forEach { bundle -> + var worker: LocalhostBooter? = null + while (worker == null) { + worker = workerPool.poll() + if (worker != null) break + delay(10) + } + val booter = worker!! + + GlobalScope.launch(context = Dispatchers.IO + job) { + logger.info { "Parsing ${bundle.id} ${counter++}/${conf.testBundlesCompat().size}" } + val bundleTests = booter.testDiscoveryClient!!.execute( + conf.testPackageRoot ?: "", + bundle.applicationClasspath ?: emptyList(), + bundle.testClasspath ?: emptyList() + ) + + bundleTests.forEach { + testBundleIdentifier.put(it, bundle) + } + discoveredTests.addAll(bundleTests) + + workerPool.offer(booter) + } + } + + job.complete() + job.join() + + workerPool.forEach { it.dispose() } + }.let { + logger.debug { "Parsing finished in ${it}ms" } + } + + return discoveredTests.toList() + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/DeviceJavaOptionsTest.kt b/vendor/vendor-junit4/vendor-junit4-core/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/DeviceJavaOptionsTest.kt new file mode 100644 index 000000000..685dcfe6e --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/DeviceJavaOptionsTest.kt @@ -0,0 +1,69 @@ +package com.malinskiy.marathon.vendor.junit4 + +import com.malinskiy.marathon.device.DevicePoolId +import com.malinskiy.marathon.execution.TestBatchResults +import com.malinskiy.marathon.execution.TestStatus +import com.malinskiy.marathon.execution.progress.ProgressReporter +import com.malinskiy.marathon.test.TestBatch +import com.malinskiy.marathon.time.SystemTimer +import com.malinskiy.marathon.vendor.junit4.parsing.RemoteJupiterTestParser +import com.malinskiy.marathon.vendor.junit4.rule.IntegrationTestRule +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.runBlocking +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.time.Clock + +class DeviceJavaOptionsTest { + @get:Rule + val temp = TemporaryFolder() + + @get:Rule + val integrationTestRule = IntegrationTestRule(temp, javaOptions = listOf("-Dfoo.property=foo foo/foo/bar/1.0"), debugBooter = false) + + private lateinit var testBundleIdentifier: Junit4TestBundleIdentifier + private lateinit var device: Junit4Device + + @Before + fun setup() { + testBundleIdentifier = Junit4TestBundleIdentifier() + device = Junit4Device(integrationTestRule.configuration, SystemTimer(Clock.systemUTC()), testBundleIdentifier) + + runBlocking { + device.prepare(integrationTestRule.configuration) + } + } + + @After + fun teardown() { + device.dispose() + } + + @Test + fun testJavaOptionsTest() { + runBlocking { + val devicePoolId = DevicePoolId("test") + + val testParser = RemoteJupiterTestParser(testBundleIdentifier) + val testList = testParser.extract(integrationTestRule.configuration) + val tests = + testList.filter { it.pkg == "com.malinskiy.marathon.vendor.junit4.integrationtests" && it.clazz == "JavaOptionsTest" } + val testBatch = TestBatch(tests) + val deferred = CompletableDeferred() + val progressReporter = ProgressReporter(integrationTestRule.configuration).apply { + testCountExpectation(devicePoolId, 1) + } + + device.execute(integrationTestRule.configuration, devicePoolId, testBatch, deferred, progressReporter) + + val results = deferred.await() + results.finished.size shouldBeEqualTo 1 + results.failed.size shouldBeEqualTo 0 + results.finished.any { it.test.method == "testSystemProperty" && it.status == TestStatus.PASSED } shouldBeEqualTo true + } + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/Junit4DeviceIntegrationTest.kt b/vendor/vendor-junit4/vendor-junit4-core/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/Junit4DeviceIntegrationTest.kt new file mode 100644 index 000000000..535c2914a --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/Junit4DeviceIntegrationTest.kt @@ -0,0 +1,221 @@ +package com.malinskiy.marathon.vendor.junit4 + +import com.malinskiy.marathon.device.DevicePoolId +import com.malinskiy.marathon.execution.TestBatchResults +import com.malinskiy.marathon.execution.TestStatus +import com.malinskiy.marathon.execution.progress.ProgressReporter +import com.malinskiy.marathon.test.TestBatch +import com.malinskiy.marathon.time.SystemTimer +import com.malinskiy.marathon.vendor.junit4.parsing.RemoteJupiterTestParser +import com.malinskiy.marathon.vendor.junit4.rule.IntegrationTestRule +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.runBlocking +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.time.Clock + +class Junit4DeviceIntegrationTest { + @get:Rule + val temp = TemporaryFolder() + + @get:Rule + val integrationTestRule = IntegrationTestRule(temp) + + private lateinit var testBundleIdentifier: Junit4TestBundleIdentifier + private lateinit var device: Junit4Device + + @Before + fun setup() { + testBundleIdentifier = Junit4TestBundleIdentifier() + device = Junit4Device(integrationTestRule.configuration, SystemTimer(Clock.systemUTC()), testBundleIdentifier) + + runBlocking { + device.prepare(integrationTestRule.configuration) + } + } + + @After + fun teardown() { + device.dispose() + } + + @Test + fun testSimpleTest() { + runBlocking { + val devicePoolId = DevicePoolId("test") + + val testParser = RemoteJupiterTestParser(testBundleIdentifier) + val testList = testParser.extract(integrationTestRule.configuration) + val tests = testList.filter { it.pkg == "com.malinskiy.marathon.vendor.junit4.integrationtests" && it.clazz == "SimpleTest" } + val testBatch = TestBatch(tests) + val deferred = CompletableDeferred() + val progressReporter = ProgressReporter(integrationTestRule.configuration).apply { + testCountExpectation(devicePoolId, 5) + } + + device.execute(integrationTestRule.configuration, devicePoolId, testBatch, deferred, progressReporter) + + val results = deferred.await() + results.finished.size shouldBeEqualTo 3 + results.failed.size shouldBeEqualTo 2 + results.finished.any { it.test.method == "testSucceeds" && it.status == TestStatus.PASSED } shouldBeEqualTo true + results.finished.any { it.test.method == "testAssumptionFails" && it.status == TestStatus.ASSUMPTION_FAILURE } shouldBeEqualTo true + results.finished.any { it.test.method == "testIgnored" && it.status == TestStatus.IGNORED } shouldBeEqualTo true + + results.failed.any { it.test.method == "testFails" && it.status == TestStatus.FAILURE } shouldBeEqualTo true + results.failed.any { it.test.method == "testFailsWithNoMessage" && it.status == TestStatus.FAILURE } shouldBeEqualTo true + } + } + + @Test + fun testClassIgnoredTest() { + runBlocking { + val devicePoolId = DevicePoolId("test") + + val testParser = RemoteJupiterTestParser(testBundleIdentifier) + val testList = testParser.extract(integrationTestRule.configuration) + val tests = testList.filter { it.pkg == "com.malinskiy.marathon.vendor.junit4.integrationtests" && it.clazz == "IgnoredTest" } + val testBatch = TestBatch(tests) + val deferred = CompletableDeferred() + val progressReporter = ProgressReporter(integrationTestRule.configuration).apply { + testCountExpectation(devicePoolId, 1) + } + + device.execute(integrationTestRule.configuration, devicePoolId, testBatch, deferred, progressReporter) + + val results = deferred.await() + results.finished.size shouldBeEqualTo 1 + results.failed.size shouldBeEqualTo 0 + val test = results.finished.first().test + test.method shouldBeEqualTo "testIgnoredTest" + test.clazz shouldBeEqualTo "IgnoredTest" + test.pkg shouldBeEqualTo "com.malinskiy.marathon.vendor.junit4.integrationtests" + } + } + + @Test + fun testParameterizedTest() { + runBlocking { + val devicePoolId = DevicePoolId("test") + + val testParser = RemoteJupiterTestParser(testBundleIdentifier) + val testList = testParser.extract(integrationTestRule.configuration) + val tests = + testList.filter { it.pkg == "com.malinskiy.marathon.vendor.junit4.integrationtests" && it.clazz == "ParameterizedTest" } + val testBatch = TestBatch(tests) + val deferred = CompletableDeferred() + val progressReporter = ProgressReporter(integrationTestRule.configuration).apply { + testCountExpectation(devicePoolId, 2) + } + + device.execute(integrationTestRule.configuration, devicePoolId, testBatch, deferred, progressReporter) + + val results = deferred.await() + results.finished.size shouldBeEqualTo 2 + results.failed.size shouldBeEqualTo 0 + + results.finished.any { it.test.method == "testShouldCapitalize[a -> A]" && it.status == TestStatus.PASSED } shouldBeEqualTo true + results.finished.any { it.test.method == "testShouldCapitalize[b -> B]" && it.status == TestStatus.PASSED } shouldBeEqualTo true + } + } + + @Test + fun testCustomParameterizedTest() { + runBlocking { + val devicePoolId = DevicePoolId("test") + + val testParser = RemoteJupiterTestParser(testBundleIdentifier) + val testList = testParser.extract(integrationTestRule.configuration) + val tests = + testList.filter { it.pkg == "com.malinskiy.marathon.vendor.junit4.integrationtests" && it.clazz == "CustomParameterizedTest" } + val testBatch = TestBatch(tests) + val deferred = CompletableDeferred() + val progressReporter = ProgressReporter(integrationTestRule.configuration).apply { + testCountExpectation(devicePoolId, 2) + } + + device.execute(integrationTestRule.configuration, devicePoolId, testBatch, deferred, progressReporter) + + val results = deferred.await() + results.finished.size shouldBeEqualTo 2 + results.failed.size shouldBeEqualTo 0 + + results.finished.any { it.test.method == "testcase1 raw" && it.status == TestStatus.PASSED } shouldBeEqualTo true + results.finished.any { it.test.method == "testcase2 raw" && it.status == TestStatus.PASSED } shouldBeEqualTo true + } + } + + @Test + fun testClassHierarchyExecution() { + runBlocking { + val devicePoolId = DevicePoolId("test") + + val testParser = RemoteJupiterTestParser(testBundleIdentifier) + val testList = testParser.extract(integrationTestRule.configuration) + val tests = testList.filter { it.pkg == "com.malinskiy.marathon.vendor.junit4.integrationtests" && it.clazz == "ChildTest" } + val testBatch = TestBatch(tests) + val deferred = CompletableDeferred() + val progressReporter = ProgressReporter(integrationTestRule.configuration).apply { + testCountExpectation(devicePoolId, 2) + } + + device.execute(integrationTestRule.configuration, devicePoolId, testBatch, deferred, progressReporter) + + val results = deferred.await() + results.finished.size shouldBeEqualTo 2 + results.failed.size shouldBeEqualTo 0 + + results.finished.any { it.test.method == "testChildPassed" && it.status == TestStatus.PASSED } shouldBeEqualTo true + results.finished.any { it.test.method == "testPasses" && it.status == TestStatus.PASSED } shouldBeEqualTo true + } + } + + @Test + fun testClassHierarchyWithAbstractParentExecution() { + runBlocking { + val devicePoolId = DevicePoolId("test") + + val testParser = RemoteJupiterTestParser(testBundleIdentifier) + val testList = testParser.extract(integrationTestRule.configuration) + val tests = + testList.filter { it.pkg == "com.malinskiy.marathon.vendor.junit4.integrationtests" && it.clazz == "ChildFromAbstractTest" } + val testBatch = TestBatch(tests) + val deferred = CompletableDeferred() + val progressReporter = ProgressReporter(integrationTestRule.configuration).apply { + testCountExpectation(devicePoolId, 2) + } + + device.execute(integrationTestRule.configuration, devicePoolId, testBatch, deferred, progressReporter) + + val results = deferred.await() + results.finished.size shouldBeEqualTo 2 + results.failed.size shouldBeEqualTo 0 + + results.finished.any { it.test.method == "testParentDidSetup" && it.status == TestStatus.PASSED } shouldBeEqualTo true + results.finished.any { it.test.method == "testParent" && it.status == TestStatus.PASSED } shouldBeEqualTo true + } + } + + @Test + fun testEmptyBatch() { + runBlocking { + val devicePoolId = DevicePoolId("test") + + val testBatch = TestBatch(emptyList()) + val deferred = CompletableDeferred() + val progressReporter = ProgressReporter(integrationTestRule.configuration).apply { + testCountExpectation(devicePoolId, 1) + } + + device.execute(integrationTestRule.configuration, devicePoolId, testBatch, deferred, progressReporter) + + val results = deferred.await() + results.finished.size shouldBeEqualTo 0 + results.failed.size shouldBeEqualTo 0 + } + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/parsing/JupiterTestParserTest.kt b/vendor/vendor-junit4/vendor-junit4-core/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/parsing/JupiterTestParserTest.kt new file mode 100644 index 000000000..e82c889df --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/parsing/JupiterTestParserTest.kt @@ -0,0 +1,107 @@ +package com.malinskiy.marathon.vendor.junit4.parsing + +import com.malinskiy.marathon.test.MetaProperty +import com.malinskiy.marathon.vendor.junit4.rule.IntegrationTestRule +import com.malinskiy.marathon.vendor.junit4.rule.ParserRule +import kotlinx.coroutines.runBlocking +import org.amshove.kluent.`should contain all` +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import com.malinskiy.marathon.test.Test as MarathonTest + +@RunWith(Parameterized::class) +class JupiterTestParserTest(private val name: String, private val expected: List) { + @get:Rule + val temp = TemporaryFolder() + + @get:Rule + val testParserRule = ParserRule() + + @get:Rule + val integrationTestRule = IntegrationTestRule(temp) + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data(): Collection?> { + return listOf( + arrayOf( + "SimpleTest", listOf( + MarathonTest( + "com.malinskiy.marathon.vendor.junit4.integrationtests", + "SimpleTest", + "testSucceeds", + emptyList() + ), + MarathonTest("com.malinskiy.marathon.vendor.junit4.integrationtests", "SimpleTest", "testFails", emptyList()), + MarathonTest( + "com.malinskiy.marathon.vendor.junit4.integrationtests", + "SimpleTest", + "testAssumptionFails", + emptyList() + ), + MarathonTest( + "com.malinskiy.marathon.vendor.junit4.integrationtests", + "SimpleTest", + "testFailsWithNoMessage", + emptyList() + ), + MarathonTest("com.malinskiy.marathon.vendor.junit4.integrationtests", "SimpleTest", "testIgnored", emptyList()), + ) + ), + arrayOf( + "CustomParameterizedTest", listOf( + MarathonTest( + "com.malinskiy.marathon.vendor.junit4.integrationtests", + "CustomParameterizedTest", + "testcase1 raw", + emptyList() + ), + MarathonTest( + "com.malinskiy.marathon.vendor.junit4.integrationtests", + "CustomParameterizedTest", + "testcase2 raw", + emptyList() + ), + ) + ), + arrayOf( + "ParameterizedTest", listOf( + MarathonTest( + "com.malinskiy.marathon.vendor.junit4.integrationtests", + "ParameterizedTest", + "testShouldCapitalize[a -> A]", + emptyList() + ), + MarathonTest( + "com.malinskiy.marathon.vendor.junit4.integrationtests", + "ParameterizedTest", + "testShouldCapitalize[b -> B]", + emptyList() + ), + ) + ), + arrayOf( + "IgnoredTest", listOf( + MarathonTest( + "com.malinskiy.marathon.vendor.junit4.integrationtests", "IgnoredTest", "testIgnoredTest", listOf( + MetaProperty(name = "org.junit.Ignore") + ) + ), + ) + ), + ) + } + } + + @Test + fun testParsing() { + runBlocking { + val tests = testParserRule.testParser.extract(integrationTestRule.configuration) + tests `should contain all` expected + } + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/rule/IntegrationTestRule.kt b/vendor/vendor-junit4/vendor-junit4-core/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/rule/IntegrationTestRule.kt new file mode 100644 index 000000000..1fc202dc9 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/rule/IntegrationTestRule.kt @@ -0,0 +1,96 @@ +package com.malinskiy.marathon.vendor.junit4.rule + +import com.malinskiy.marathon.execution.Configuration +import com.malinskiy.marathon.vendor.junit4.configuration.Junit4Configuration +import com.malinskiy.marathon.vendor.junit4.configuration.executor.LocalExecutorConfiguration +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +class IntegrationTestRule( + private val temp: TemporaryFolder, + private val javaOptions: List = emptyList(), + private val debugBooter: Boolean = false, + private val debugExecutor: Boolean = false, +) : TestRule { + lateinit var configuration: Configuration + + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + val deps = listOf( + "/junit4-integration-tests.jar", + "/fixtures/junit4/junit-4.13.2.jar", + "/fixtures/junit4/hamcrest-core-1.3.jar", + "/fixtures/junit4/kotlin-stdlib-common-1.4.31.jar", + "/fixtures/junit4/kotlin-stdlib-1.4.31.jar", + "/fixtures/junit4/kotlin-stdlib-jdk7-1.4.31.jar", + "/fixtures/junit4/kotlin-stdlib-jdk8-1.4.31.jar", + ).map { + temp.newFile().apply { + outputStream().use { tempFile -> + javaClass.getResourceAsStream(it).copyTo(tempFile) + } + } + } + + val junit4Configuration = Junit4Configuration( + applicationClasspath = null, + testClasspath = deps, + testBundles = null, + testPackageRoot = "com.malinskiy.marathon.vendor.junit4.integrationtests", + debugBooter = debugBooter, + forkEvery = 0, + executorConfiguration = LocalExecutorConfiguration(parallelism = 1, debug = debugExecutor, javaOptions = javaOptions) + ) + + configuration = Configuration( + name = "junit4 test configuration", + outputDir = temp.newFolder("marathon-report"), + analyticsConfiguration = null, + poolingStrategy = null, + shardingStrategy = null, + sortingStrategy = null, + batchingStrategy = null, + flakinessStrategy = null, + retryStrategy = null, + filteringConfiguration = null, + ignoreFailures = null, + isCodeCoverageEnabled = null, + fallbackToScreenshots = null, + strictMode = null, + uncompletedTestRetryQuota = null, + testClassRegexes = null, + includeSerialRegexes = null, + excludeSerialRegexes = null, + testBatchTimeoutMillis = null, + debug = true, + testOutputTimeoutMillis = null, + screenRecordingPolicy = null, + vendorConfiguration = junit4Configuration, + analyticsTracking = null, + deviceInitializationTimeoutMillis = null, + ) + + base.evaluate() + } + + private fun killAll() { + killOnPort(1044) + killOnPort(1045) + killOnPort(50051) + } + + private fun killOnPort(port: Int) { + val listProcess = Runtime.getRuntime().exec("lsof -t -i :$port") + val output = listProcess.inputStream.bufferedReader().readText() + listProcess.waitFor() + val pid = output.trim() + if (pid.isNotEmpty()) { + Runtime.getRuntime().exec("kill $pid").waitFor() + } + } + } + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-core/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/rule/ParserRule.kt b/vendor/vendor-junit4/vendor-junit4-core/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/rule/ParserRule.kt new file mode 100644 index 000000000..a6d4d3f08 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-core/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/rule/ParserRule.kt @@ -0,0 +1,30 @@ +package com.malinskiy.marathon.vendor.junit4.rule + +import com.malinskiy.marathon.execution.TestParser +import com.malinskiy.marathon.vendor.junit4.Junit4TestBundleIdentifier +import com.malinskiy.marathon.vendor.junit4.parsing.JupiterTestParser +import com.malinskiy.marathon.vendor.junit4.parsing.RemoteJupiterTestParser +import org.junit.rules.MethodRule +import org.junit.runners.model.FrameworkMethod +import org.junit.runners.model.Statement + +class ParserRule : MethodRule { + private lateinit var internalTestParser: TestParser + + override fun apply(base: Statement?, method: FrameworkMethod?, target: Any?): Statement { + return object : Statement() { + override fun evaluate() { + var bundleIdentifier = Junit4TestBundleIdentifier() + internalTestParser = JupiterTestParser(bundleIdentifier) + base?.evaluate() + + bundleIdentifier = Junit4TestBundleIdentifier() + internalTestParser = RemoteJupiterTestParser(bundleIdentifier) + base?.evaluate() + } + } + } + + val testParser: TestParser + get() = internalTestParser +} diff --git a/vendor/vendor-junit4/vendor-junit4-integration-tests/build.gradle.kts b/vendor/vendor-junit4/vendor-junit4-integration-tests/build.gradle.kts new file mode 100644 index 000000000..6622b5737 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-integration-tests/build.gradle.kts @@ -0,0 +1,25 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + id("org.jetbrains.kotlin.jvm") +} + +dependencies { + implementation(Libraries.kotlinStdLib) + implementation(TestLibraries.junit) +} + +tasks.withType { + kotlinOptions.jvmTarget = "1.8" + kotlinOptions.apiVersion = "1.4" +} + +tasks.withType { + enabled = false +} + +tasks.register("testJar") { + archiveFileName.set("junit4-integration-tests.jar") + from(project.the()["test"].output) +} diff --git a/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/AbstractParentTest.kt b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/AbstractParentTest.kt new file mode 100644 index 000000000..04ac78d7a --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/AbstractParentTest.kt @@ -0,0 +1,19 @@ +package com.malinskiy.marathon.vendor.junit4.integrationtests + +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +abstract class AbstractParentTest { + protected lateinit var someParameter: String + + @Before + fun setup() { + someParameter = "fake" + } + + @Test + fun testParent() { + Assert.assertEquals(someParameter, "fake") + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/ChildFromAbstractTest.kt b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/ChildFromAbstractTest.kt new file mode 100644 index 000000000..cfdc0d009 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/ChildFromAbstractTest.kt @@ -0,0 +1,11 @@ +package com.malinskiy.marathon.vendor.junit4.integrationtests + +import org.junit.Assert +import org.junit.Test + +class ChildFromAbstractTest : AbstractParentTest() { + @Test + fun testParentDidSetup() { + Assert.assertEquals(someParameter, "fake") + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/ChildTest.kt b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/ChildTest.kt new file mode 100644 index 000000000..72afc0917 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/ChildTest.kt @@ -0,0 +1,11 @@ +package com.malinskiy.marathon.vendor.junit4.integrationtests + +import org.junit.Assert +import org.junit.Test + +class ChildTest : ParentTest() { + @Test + fun testChildPassed() { + Assert.assertTrue(true) + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/CustomParameterizedTest.kt b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/CustomParameterizedTest.kt new file mode 100644 index 000000000..b5bd11321 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/CustomParameterizedTest.kt @@ -0,0 +1,9 @@ +package com.malinskiy.marathon.vendor.junit4.integrationtests + +import com.malinskiy.marathon.vendor.junit4.integrationtests.custom.Functional +import org.junit.runner.RunWith + +@RunWith(Functional::class) +@Functional.Properties("custom-parameterized") +class CustomParameterizedTest { +} diff --git a/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/IgnoredTest.kt b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/IgnoredTest.kt new file mode 100644 index 000000000..a9153979a --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/IgnoredTest.kt @@ -0,0 +1,12 @@ +package com.malinskiy.marathon.vendor.junit4.integrationtests + +import org.junit.Ignore +import org.junit.Test + +@Ignore +class IgnoredTest { + @Test + fun testIgnoredTest() { + assert(false) { "should be ignored" } + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/JavaOptionsTest.kt b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/JavaOptionsTest.kt new file mode 100644 index 000000000..ebd9e3c0f --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/JavaOptionsTest.kt @@ -0,0 +1,16 @@ +package com.malinskiy.marathon.vendor.junit4.integrationtests + +import org.junit.Assert +import org.junit.Test + +class JavaOptionsTest { + @Test + fun testSystemProperty() { + val fooProperty = System.getProperty("foo.property") + Assert.assertTrue( + "incorrect foo.property; " + + "Marathon should have passed this in correctly", + "foo foo/foo/bar/1.0" == fooProperty + ) + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/ParameterizedTest.kt b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/ParameterizedTest.kt new file mode 100644 index 000000000..db3f694ed --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/ParameterizedTest.kt @@ -0,0 +1,26 @@ +package com.malinskiy.marathon.vendor.junit4.integrationtests + +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import java.util.Arrays + +@RunWith(Parameterized::class) +class ParameterizedTest(private val input: String, private val expected: String) { + + @Test + fun testShouldCapitalize() { + assert(input.capitalize() == expected) + } + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{0} -> {1}") + fun scenarios(): Collection>? { + return Arrays.asList( + arrayOf("a", "A"), + arrayOf("b", "B"), + ) + } + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/ParentTest.kt b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/ParentTest.kt new file mode 100644 index 000000000..ac60b50b5 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/ParentTest.kt @@ -0,0 +1,11 @@ +package com.malinskiy.marathon.vendor.junit4.integrationtests + +import org.junit.Assert +import org.junit.Test + +open class ParentTest { + @Test + fun testPasses() { + Assert.assertTrue(true) + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/SimpleTest.kt b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/SimpleTest.kt new file mode 100644 index 000000000..5585406ea --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/SimpleTest.kt @@ -0,0 +1,34 @@ +package com.malinskiy.marathon.vendor.junit4.integrationtests + +import org.junit.Assert +import org.junit.Assume.assumeFalse +import org.junit.Ignore +import org.junit.Test + +class SimpleTest { + @Test + fun testSucceeds() { + Assert.assertTrue(true) + } + + @Test + fun testFails() { + Assert.fail("Expected failure") + } + + @Test + fun testFailsWithNoMessage() { + Assert.fail() + } + + @Test + fun testAssumptionFails() { + assumeFalse(true) + } + + @Ignore + @Test + fun testIgnored() { + Assert.fail("Should not happen") + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/custom/Functional.kt b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/custom/Functional.kt new file mode 100644 index 000000000..8c2a06366 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/kotlin/com/malinskiy/marathon/vendor/junit4/integrationtests/custom/Functional.kt @@ -0,0 +1,132 @@ +package com.malinskiy.marathon.vendor.junit4.integrationtests.custom + +import org.junit.Assert +import org.junit.runner.Description +import org.junit.runner.notification.RunNotifier +import org.junit.runners.ParentRunner +import org.junit.runners.model.Statement +import org.junit.runners.model.TestClass +import java.io.IOException +import java.util.Collections +import java.util.Properties + +class Functional(klass: Class<*>) : ParentRunner(klass) { + private val tests: List + + override fun getChildren(): List { + return tests + } + + override fun describeChild(child: Test): Description { + return child.description() + } + + override fun runChild(child: Test, notifier: RunNotifier) { + runLeaf(child, child.description(), notifier) + } + + class Factory constructor(klass: Class<*>) { + val testClass: TestClass + val properties: Map + fun createTests(): List { + return properties.keys + .filter { it.startsWith("input") } + .map { it.substringAfter("input.") } + .map { baseTestName -> + createFunctionalTest( + baseTestName + ) + }.toList() + } + + private fun createFunctionalTest(baseTestName: String): Test { + return Test(this, baseTestName, false) + } + + companion object { + private fun getTestPropertiesName(testClass: TestClass): String { + val annotation = testClass.getAnnotation(Properties::class.java) + ?: throw AssertionError("@Properties: required class annotation not found") + return annotation.value + } + } + + init { + testClass = TestClass(klass) + properties = readProperties(getTestPropertiesName(testClass)) + } + } + + class Test internal constructor(factory: Factory, testName: String, withEscaping: Boolean) : Statement() { + private val input: String + private val expected: String + private val description: Description + override fun evaluate() { + Assert.assertEquals(input.capitalize(), expected) + } + + fun description(): Description { + return description + } + + companion object { + private fun property(factory: Factory, key: String): String { + return factory.properties[key] ?: throw AssertionError("Property $key not found ") + } + } + + init { + input = property(factory, "input.$testName") + expected = property(factory, "output.$testName") + description = Description.createTestDescription( + factory.testClass.javaClass, + testName + if (withEscaping) " escaped" else " raw" + ) + } + } + + /** + * Annotation for the `public static final String` field that declares the properties file to use. + * This annotation is required. + */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) + annotation class Properties( + /** + * The name of the properties file to load. + * + * @return The name of the `.properties` file that defines the individual tests + */ + val value: String + ) + + companion object { + private const val PROP_PREFIX = "/functional-tests/" + private const val PROP_SUFFIX = "-functional-tests.properties" + private fun readProperties(testsFile: String): Map { + try { + Functional::class.java.getResourceAsStream(PROP_PREFIX + testsFile + PROP_SUFFIX).use { stream -> + if (stream == null) { + throw AssertionError("Could not load tests for $testsFile") + } + val properties = Properties() + properties.load(stream) + val map: MutableMap = HashMap() + properties.forEach { k: Any, v: Any -> + map[k as String] = v as String + } + return map + } + } catch (e: IOException) { + throw AssertionError("Error loading tests for $testsFile", e) + } + } + } + + /** + * Only called reflectively. Do not use programmatically. + */ + init { + tests = Collections.unmodifiableList(Factory(klass).createTests()) + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/resources/functional-tests/custom-parameterized-functional-tests.properties b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/resources/functional-tests/custom-parameterized-functional-tests.properties new file mode 100644 index 000000000..b2fc1cee9 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-integration-tests/src/test/resources/functional-tests/custom-parameterized-functional-tests.properties @@ -0,0 +1,4 @@ +input.testcase1=a +output.testcase1=A +input.testcase2=b +output.testcase2=B diff --git a/vendor/vendor-junit4/vendor-junit4-runner-contract/build.gradle.kts b/vendor/vendor-junit4/vendor-junit4-runner-contract/build.gradle.kts new file mode 100644 index 000000000..e1e91b524 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-runner-contract/build.gradle.kts @@ -0,0 +1,50 @@ +import com.google.protobuf.gradle.builtins +import com.google.protobuf.gradle.generateProtoTasks +import com.google.protobuf.gradle.id +import com.google.protobuf.gradle.plugins +import com.google.protobuf.gradle.protobuf +import com.google.protobuf.gradle.protoc +import com.google.protobuf.gradle.remove + +plugins { + `java-library` + id("com.google.protobuf") version Versions.protobufGradle + id("idea") + id("com.github.johnrengelman.shadow") version "6.1.0" +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${Versions.protobuf}" + } + generateProtoTasks { + all().forEach { + it.builtins { + remove("java") + } + it.plugins { + id("java") { + option("lite") + } + } + } + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.create("relocateShadowJar") { + target = tasks["shadowJar"] as com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + prefix = "com.malinskiy.marathon.vendor.junit4.shadows" +} + +tasks.named("shadowJar").configure { + dependsOn(tasks["relocateShadowJar"]) +} + +dependencies { + api(Libraries.protobufLite) +} diff --git a/vendor/vendor-junit4/vendor-junit4-runner-contract/src/main/proto/control.proto b/vendor/vendor-junit4/vendor-junit4-runner-contract/src/main/proto/control.proto new file mode 100644 index 000000000..d85b5f559 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-runner-contract/src/main/proto/control.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +package com.malinskiy.marathon.vendor.junit4.runner.contract; + +option java_multiple_files = true; +option java_package = "com.malinskiy.marathon.vendor.junit4.runner.contract"; +option java_outer_classname = "Protocol"; + +message TestIdentifier { + string fqtn = 1; +} + +message Message { + Type type = 1; + sint32 testCount = 2; + sint64 totalDurationMillis = 3; + string classname = 4; + string method = 5; + string message = 6; + string stacktrace = 7; + + enum Type { + RUN_STARTED = 0; + RUN_FINISHED = 1; + TEST_STARTED = 2; + TEST_FINISHED = 3; + TEST_FAILURE = 4; + TEST_ASSUMPTION_FAILURE = 5; + TEST_IGNORED = 6; + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-runner/build.gradle.kts b/vendor/vendor-junit4/vendor-junit4-runner/build.gradle.kts new file mode 100644 index 000000000..0e344e021 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-runner/build.gradle.kts @@ -0,0 +1,15 @@ + +plugins { + `java-library` + id("com.github.johnrengelman.shadow") version "6.1.0" +} + +dependencies { + api(project(":vendor:vendor-junit4:vendor-junit4-runner-contract", configuration = "shadow")) + compileOnly(TestLibraries.junit) +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} diff --git a/vendor/vendor-junit4/vendor-junit4-runner/src/main/java/com/malinskiy/marathon/vendor/junit4/runner/ListenerAdapter.java b/vendor/vendor-junit4/vendor-junit4-runner/src/main/java/com/malinskiy/marathon/vendor/junit4/runner/ListenerAdapter.java new file mode 100644 index 000000000..be326ff15 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-runner/src/main/java/com/malinskiy/marathon/vendor/junit4/runner/ListenerAdapter.java @@ -0,0 +1,122 @@ +package com.malinskiy.marathon.vendor.junit4.runner; + +import com.malinskiy.marathon.vendor.junit4.runner.contract.Message; +import org.junit.runner.Description; +import org.junit.runner.Result; +import org.junit.runner.notification.Failure; +import org.junit.runner.notification.RunListener; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; + +public class ListenerAdapter extends RunListener { + final private Socket socket; + + public ListenerAdapter(Socket socket) { + this.socket = socket; + } + + @Override + public void testRunStarted(Description description) throws Exception { + super.testRunStarted(description); + send(socket, Message.newBuilder() + .setType(Message.Type.RUN_STARTED) + .build() + ); + } + + @Override + public void testRunFinished(Result result) throws Exception { + super.testRunFinished(result); + send(socket, Message.newBuilder() + .setType(Message.Type.RUN_FINISHED) + .setTotalDurationMillis(result.getRunTime()) + .build() + ); + } + + @Override + public void testSuiteStarted(Description description) throws Exception { + super.testSuiteStarted(description); + } + + @Override + public void testSuiteFinished(Description description) throws Exception { + super.testSuiteFinished(description); + } + + @Override + public void testStarted(Description description) throws Exception { + super.testStarted(description); + send(socket, Message.newBuilder() + .setType(Message.Type.TEST_STARTED) + .setClassname(description.getClassName()) + .setMethod(description.getMethodName()) + .build() + ); + } + + @Override + public void testFinished(Description description) throws Exception { + super.testFinished(description); + send(socket, Message.newBuilder() + .setType(Message.Type.TEST_FINISHED) + .setClassname(description.getClassName()) + .setMethod(description.getMethodName()) + .build() + ); + } + + @Override + public void testFailure(Failure failure) throws Exception { + super.testFailure(failure); + String failureMessage = failure.getMessage(); + if (failureMessage == null) { + failureMessage = "No failure message provided"; + } + + send(socket, Message.newBuilder() + .setType(Message.Type.TEST_FAILURE) + .setClassname(failure.getDescription().getClassName()) + .setMethod(failure.getDescription().getMethodName()) + .setMessage(failureMessage) + .setStacktrace(failure.getTrace()) + .build() + ); + } + + @Override + public void testAssumptionFailure(Failure failure) { + super.testAssumptionFailure(failure); + send(socket, Message.newBuilder() + .setType(Message.Type.TEST_ASSUMPTION_FAILURE) + .setClassname(failure.getDescription().getClassName()) + .setMethod(failure.getDescription().getMethodName()) + .setMessage(failure.getMessage()) + .setStacktrace(failure.getTrace()) + .build() + ); + } + + @Override + public void testIgnored(Description description) throws Exception { + super.testIgnored(description); + send(socket, Message.newBuilder() + .setType(Message.Type.TEST_IGNORED) + .setClassname(description.getClassName()) + .setMethod(description.getMethodName()) + .build() + ); + } + + private void send(Socket socket, Message message) { + try { + OutputStream stream = socket.getOutputStream(); + message.writeDelimitedTo(stream); + } + catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-runner/src/main/java/com/malinskiy/marathon/vendor/junit4/runner/Runner.java b/vendor/vendor-junit4/vendor-junit4-runner/src/main/java/com/malinskiy/marathon/vendor/junit4/runner/Runner.java new file mode 100644 index 000000000..e2fd6f072 --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-runner/src/main/java/com/malinskiy/marathon/vendor/junit4/runner/Runner.java @@ -0,0 +1,95 @@ +package com.malinskiy.marathon.vendor.junit4.runner; + +import com.malinskiy.marathon.vendor.junit4.runner.contract.TestIdentifier; +import org.junit.runner.Description; +import org.junit.runner.JUnitCore; +import org.junit.runner.Request; + +import java.io.*; +import java.net.InetAddress; +import java.net.Socket; +import java.util.*; + +public class Runner { + public static void main(String[] args) throws Exception { + Map environ = System.getenv(); + File filterFile = new File(environ.get("FILTER")); + String outputSocket = environ.get("OUTPUT"); + Integer port = getInteger(outputSocket); + + try (Socket socket = new Socket(InetAddress.getLocalHost(), port)) { + ListenerAdapter adapter = new ListenerAdapter(socket); + + List tests = new ArrayList<>(); + try (InputStream stream = new BufferedInputStream(new FileInputStream(filterFile))) { + TestIdentifier identifier; + while ((identifier = TestIdentifier.parseDelimitedFrom(stream)) != null) { + tests.add(identifier.getFqtn()); + } + } + catch (FileNotFoundException e) { + e.printStackTrace(); + } + + Set> klasses = new HashSet<>(); + Set testDescriptions = new HashSet<>(); + + for (String fqtn : tests) { + String klass = fqtn.substring(0, fqtn.indexOf('#')); + Class testClass = null; + try { + testClass = Class.forName(klass); + } + catch (ClassNotFoundException e) { + failVerbously(socket, e); + } + klasses.add(testClass); + + String method = fqtn.substring(fqtn.indexOf('#') + 1); + testDescriptions.add(Description.createTestDescription(testClass, method)); + } + + Map actualClassLocator = new HashMap<>(); + TestFilter testFilter = new TestFilter(testDescriptions, actualClassLocator); + + Request request = Request.classes(klasses.toArray(new Class[] {})) + .filterWith(testFilter); + + JUnitCore core = new JUnitCore(); + try { + core.addListener(adapter); + core.run(request); + core.removeListener(adapter); + System.exit(0); + } + catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + } + catch (Exception e) { + failMiserably(e); + System.exit(1); + } + } + + private static Integer getInteger(String outputSocket) throws Exception { + Integer port = 0; + try { + port = Integer.valueOf(outputSocket); + } + catch (NumberFormatException e) { + failMiserably(e); + } + return port; + } + + private static void failMiserably(Exception e) throws Exception { + e.printStackTrace(); + throw e; + } + + private static void failVerbously(Socket socket, Exception e) throws Exception { + failMiserably(e); + } +} diff --git a/vendor/vendor-junit4/vendor-junit4-runner/src/main/java/com/malinskiy/marathon/vendor/junit4/runner/TestFilter.java b/vendor/vendor-junit4/vendor-junit4-runner/src/main/java/com/malinskiy/marathon/vendor/junit4/runner/TestFilter.java new file mode 100644 index 000000000..df1955b8e --- /dev/null +++ b/vendor/vendor-junit4/vendor-junit4-runner/src/main/java/com/malinskiy/marathon/vendor/junit4/runner/TestFilter.java @@ -0,0 +1,71 @@ +package com.malinskiy.marathon.vendor.junit4.runner; + +import org.junit.runner.Description; +import org.junit.runner.manipulation.Filter; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class TestFilter extends Filter { + private Set descriptions; + private Map classLocator; + final private String CLASS_NAME_STUB = "org.junit.runners.model.TestClass"; + private Set verifiedChildren; + + public TestFilter(Set descriptions, Map classLocator) { + this.descriptions = descriptions; + this.classLocator = classLocator; + verifiedChildren = new HashSet<>(); + } + + @Override + public boolean shouldRun(Description description) { + if (verifiedChildren.contains(description)) { + return true; + } + else { + return shouldRun(description, null); + } + } + + private boolean shouldRun(Description description, String className) { + if (description.isTest()) { + /** + * Handling for parameterized tests that report org.junit.runners.model.TestClass as their test class + */ + Description verificationDescription; + if (description.getClassName().equals(CLASS_NAME_STUB) && className != null) { + verificationDescription = + Description.createTestDescription(className, description.getMethodName(), description.getAnnotations().toArray()); + } + else { + verificationDescription = description; + } + Boolean contains = descriptions.contains(verificationDescription); + if (contains) { + verifiedChildren.add(description); + if (description.getClassName().equals(CLASS_NAME_STUB) && className != null) { + classLocator.put(description, className); + } + } + + return contains; + } + + // explicitly check if any children want to run + Boolean childrenResult = false; + for (Description each : description.getChildren()) { + if (shouldRun(each, description.getClassName())) { + childrenResult = true; + } + } + + return childrenResult; + } + + @Override + public String describe() { + return "Marathon JUnit4 execution filter"; + } +} diff --git a/vendor/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/MarathonFactoryExtensions.kt b/vendor/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/MarathonFactoryExtensions.kt index f08e7ee69..0f6b3a06e 100644 --- a/vendor/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/MarathonFactoryExtensions.kt +++ b/vendor/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/MarathonFactoryExtensions.kt @@ -2,8 +2,12 @@ package com.malinskiy.marathon.test import com.malinskiy.marathon.Marathon import com.malinskiy.marathon.test.factory.MarathonFactory +import kotlinx.coroutines.runBlocking -fun setupMarathon(f: MarathonFactory.() -> Unit): Marathon { - val marathonFactory = MarathonFactory() - return marathonFactory.apply(f).build() +fun setupMarathon(f: suspend MarathonFactory.() -> Unit): Marathon { + return runBlocking { + val marathonFactory = MarathonFactory() + f(marathonFactory) + marathonFactory.build() + } } diff --git a/vendor/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/ConfigurationFactory.kt b/vendor/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/ConfigurationFactory.kt index 3b7486578..7808b827b 100644 --- a/vendor/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/ConfigurationFactory.kt +++ b/vendor/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/ConfigurationFactory.kt @@ -42,7 +42,7 @@ class ConfigurationFactory { var screenRecordingPolicy: ScreenRecordingPolicy? = null var deviceInitializationTimeoutMillis: Long? = null - fun tests(block: () -> List) { + suspend fun tests(block: () -> List) { val testParser = vendorConfiguration.testParser() whenever(testParser.extract(any())).thenReturn(block.invoke()) } diff --git a/vendor/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/MarathonFactory.kt b/vendor/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/MarathonFactory.kt index 6a959537d..883f84782 100644 --- a/vendor/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/MarathonFactory.kt +++ b/vendor/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/MarathonFactory.kt @@ -17,7 +17,9 @@ class MarathonFactory { var timer: Timer? = null - fun configuration(block: ConfigurationFactory.() -> Unit) = configurationFactory.apply(block) + suspend fun configuration(block: suspend ConfigurationFactory.() -> Unit) { + block(configurationFactory) + } fun build(): Marathon { val configuration = configurationFactory.build()