diff --git a/build.gradle.kts b/build.gradle.kts index 2495922e..9eadd723 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,9 +19,8 @@ dependencies { api("com.dorongold.plugins:task-tree:4.0.0") api("guru.nidi:graphviz-kotlin:0.18.1") api("com.hierynomus:sshj:0.38.0") - - testApi("org.junit.jupiter:junit-jupiter-api:5.10.3") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.3") + api("org.junit.platform:junit-platform-launcher:1.10.3") + testImplementation("org.junit.jupiter:junit-jupiter:5.10.3") } tasks.withType { diff --git a/src/main/kotlin/us/ihmc/build/IHMCBuildPlugin.kt b/src/main/kotlin/us/ihmc/build/IHMCBuildPlugin.kt index 7ad77aa7..94559f9e 100644 --- a/src/main/kotlin/us/ihmc/build/IHMCBuildPlugin.kt +++ b/src/main/kotlin/us/ihmc/build/IHMCBuildPlugin.kt @@ -14,6 +14,7 @@ import org.gradle.plugins.ide.eclipse.EclipsePlugin import org.gradle.plugins.ide.idea.IdeaPlugin import us.ihmc.cd.AppExtension import us.ihmc.cd.RemoteExtension +import us.ihmc.ci.IHMCCIPlugin class IHMCBuildPlugin : Plugin { @@ -122,5 +123,12 @@ class IHMCBuildPlugin : Plugin // IHMCBuildTools.defineExtraSourceSetCompositeTask("publishExtraSourceSets", arrayListOf("publish"), project) IHMCBuildTools.defineDynamicCompositeTask(project) + + // Apply old ihmc-ci plugin functionality + if (!project.hasProperty("isProjectGroup")) + { + val ciPlugin = IHMCCIPlugin() + ciPlugin.apply(project); + } } } \ No newline at end of file diff --git a/src/main/kotlin/us/ihmc/ci/AllocationInstrumenter.kt b/src/main/kotlin/us/ihmc/ci/AllocationInstrumenter.kt new file mode 100644 index 00000000..5f58ea7e --- /dev/null +++ b/src/main/kotlin/us/ihmc/ci/AllocationInstrumenter.kt @@ -0,0 +1,6 @@ +package us.ihmc.ci + +class AllocationInstrumenter(val version: String) +{ + fun instrumenter(): String = "com.google.code.java-allocation-instrumenter:java-allocation-instrumenter:$version" +} \ No newline at end of file diff --git a/src/main/kotlin/us/ihmc/ci/IHMCCICategoriesExtension.kt b/src/main/kotlin/us/ihmc/ci/IHMCCICategoriesExtension.kt new file mode 100644 index 00000000..b38efd68 --- /dev/null +++ b/src/main/kotlin/us/ihmc/ci/IHMCCICategoriesExtension.kt @@ -0,0 +1,45 @@ +package us.ihmc.ci + +import org.gradle.api.Project + +val ALLOCATION_AGENT_KEY = "allocationAgent" + +class IHMCCICategory(val name: String) +{ + var forkEvery = 0 // no limit + var maxParallelForks = 1 // careful, cost of spawning JVMs is high + var junit5ParallelEnabled = false // doesn't work right now with Gradle's test runner. See: https://github.com/gradle/gradle/issues/6453 + var junit5ParallelStrategy = "fixed" + var junit5ParallelFixedParallelism = 1 + val excludeTags = hashSetOf() + val includeTags = hashSetOf() + val jvmProperties = hashMapOf() + val jvmArguments = hashSetOf() + var minHeapSizeGB = 1 + var maxHeapSizeGB = 4 + var enableAssertions = true + var defaultTimeout = 1200 // 20 minutes + var testTaskTimeout = 1800 // 30 minutes + var doFirst: () -> Unit = {} // run user code when this category is selected +} + +open class IHMCCICategoriesExtension(private val project: Project) +{ + val categories = hashMapOf() + + fun configure(name: String, configuration: IHMCCICategory.() -> Unit) + { + configuration.invoke(configure(name)) + } + + fun configure(name: String): IHMCCICategory + { + val category = categories.getOrPut(name, { IHMCCICategory(name) }) + if (name != "all" && name != "fast") // all require no includes or excludes, fast will be configured later + { + category.includeTags += name // by default, include tags of the category name + } + + return category + } +} \ No newline at end of file diff --git a/src/main/kotlin/us/ihmc/ci/IHMCCIPlugin.kt b/src/main/kotlin/us/ihmc/ci/IHMCCIPlugin.kt new file mode 100644 index 00000000..43841fa2 --- /dev/null +++ b/src/main/kotlin/us/ihmc/ci/IHMCCIPlugin.kt @@ -0,0 +1,397 @@ +package us.ihmc.ci; + +import org.gradle.api.GradleException +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.plugins.JavaPluginConvention +import org.gradle.api.tasks.testing.Test +import org.gradle.api.tasks.testing.logging.TestLogEvent +import us.ihmc.build.IHMCBuildLogTools +import java.io.File +import java.time.Duration + +lateinit var LogTools: IHMCBuildLogTools + +/** + * Included now in IHMCBuildPlugin + */ +class IHMCCIPlugin : Plugin +{ + val JUNIT_VERSION = "5.9.2" + val PLATFORM_VERSION = "1.9.2" + val ALLOCATION_INSTRUMENTER_VERSION = "3.3.0" + + lateinit var project: Project + var cpuThreads = 8 + var category: String = "fast" + object Unset + var minHeapSizeGBOverride: Any = Unset + var maxHeapSizeGBOverride: Any = Unset + var forkEveryOverride: Any = Unset + var maxParallelForksOverride: Any = Unset + var enableAssertionsOverride: Any = Unset + var defaultTimeoutOverride: Any = Unset + var testTaskTimeoutOverride: Any = Unset + var allocationRecordingOverride: Any = Unset + lateinit var categoriesExtension: IHMCCICategoriesExtension + var allocationJVMArg: String? = null + val apiConfigurationName = "api" + val runtimeConfigurationName = "runtimeOnly" + val addedDependenciesMap = HashMap() + var configuredTestTasks = HashMap() + val testProjects = lazy { + val testProjects = arrayListOf() + for (allproject in project.allprojects) + { + if (allproject.name.endsWith("-test")) + { + testProjects += allproject + } + } + testProjects + } + val testsToTagsMap = lazy { + val map = hashMapOf>() + testProjects.value.forEach { + TagParser.parseForTags(it, map) + } + map + } + private val junit = JUnitExtension(JUNIT_VERSION, PLATFORM_VERSION) + private val allocation = AllocationInstrumenter(ALLOCATION_INSTRUMENTER_VERSION) + + override fun apply(project: Project) + { + this.project = project + LogTools = IHMCBuildLogTools(project.logger) + + loadProperties() + categoriesExtension = project.extensions.create("categories", IHMCCICategoriesExtension::class.java, project) + project.extensions.add("junit", junit) + project.extensions.add("allocation", allocation) + + // These things must be configured later in the build lifecycle. + // Here, we are notified when any task is added to the build. + // This happens a lot, so must check if requirements are met + // and gate it with a boolean. + project.tasks.whenTaskAdded { + for (testProject in testProjects.value) + { + addDependencies(testProject, apiConfigurationName, runtimeConfigurationName) + configureTestTask(testProject) + } +// if (!containsIHMCTestMultiProject(project)) +// { +// addDependencies(project, "testImplementation", "testRuntimeOnly") +// configureTestTask(project) +// } +// +// var allHaveCompileJava = true +// testProjects.value.forEach { testProject -> +// allHaveCompileJava = allHaveCompileJava && testProject.tasks.findByPath("compileJava") != null +// } + } + } + + private fun addDependencies(project: Project, apiConfigurationName: String, runtimeConfigurationName: String) + { + addedDependenciesMap.computeIfAbsent("${project.name}:$apiConfigurationName") { false } + addedDependenciesMap.computeIfAbsent("${project.name}:$runtimeConfigurationName") { false } + + // add runtime dependencies + if (!addedDependenciesMap["${project.name}:$runtimeConfigurationName"]!! && configurationExists(project, runtimeConfigurationName)) + { + addedDependenciesMap["${project.name}:$runtimeConfigurationName"] = true + LogTools.info("Adding JUnit 5 dependencies to $runtimeConfigurationName in ${project.name}") + project.dependencies.add(runtimeConfigurationName, junit.jupiterEngine()) + } + + // add api dependencies + if (!addedDependenciesMap["${project.name}:$apiConfigurationName"]!! && configurationExists(project, apiConfigurationName)) + { + addedDependenciesMap["${project.name}:$apiConfigurationName"] = true + LogTools.info("Adding JUnit 5 dependencies to $apiConfigurationName in ${project.name}") + project.dependencies.add(apiConfigurationName, junit.jupiterApi()) + project.dependencies.add(apiConfigurationName, junit.platformCommons()) + project.dependencies.add(apiConfigurationName, junit.platformLauncher()) + + if (category == "allocation") // help out users trying to run allocation tests + { + LogTools.info("Adding allocation instrumenter dependency to $apiConfigurationName in ${project.name}") + project.dependencies.add(apiConfigurationName, allocation.instrumenter()) + } + } + } + + private fun configurationExists(project: Project, name: String): Boolean + { + for (configuration in project.configurations) + { + if (configuration.name == name) + { + return true + } + } + return false + } + + fun configureTestTask(project: Project) + { + configuredTestTasks.computeIfAbsent(project.name) { false } + + if (!configuredTestTasks[project.name]!! && project.tasks.findByName("test") != null) + { + configuredTestTasks[project.name] = true + val addPhonyTestXmlTask = addPhonyTestXmlTask(project) + project.tasks.named("test", Test::class.java) { + // create a default category if not found + val categoryConfig = postProcessCategoryConfig() + applyCategoryConfigToGradleTest(this, categoryConfig) + + doFirst { + val test: Test = this as Test + test.setForkEvery(categoryConfig.forkEvery.toLong()) + test.maxParallelForks = categoryConfig.maxParallelForks + this.project.properties["runningOnCIServer"].run { + if (this != null) + test.systemProperties["runningOnCIServer"] = toString() + } + for (jvmProp in categoryConfig.jvmProperties) + { + test.systemProperties[jvmProp.key] = jvmProp.value + } + test.systemProperties["junit.jupiter.execution.timeout.default"] = categoryConfig.defaultTimeout + test.timeout.set(Duration.ofSeconds(categoryConfig.testTaskTimeout.toLong())) + + if (categoryConfig.junit5ParallelEnabled) + { + test.systemProperties["junit.jupiter.execution.parallel.enabled"] = "true" + test.systemProperties["junit.jupiter.execution.parallel.config.strategy"] = categoryConfig.junit5ParallelStrategy + test.systemProperties["junit.jupiter.execution.parallel.config.fixed.parallelism"] = categoryConfig.junit5ParallelFixedParallelism + } + + val java = project.convention.getPlugin(JavaPluginConvention::class.java) + val resourcesDir = java.sourceSets.getByName("main").output.resourcesDir + LogTools.info("Passing to JVM: -Dresource.dir=$resourcesDir") + test.systemProperties["resource.dir"] = resourcesDir + + for (jvmArg in categoryConfig.jvmArguments) + { + if (jvmArg == ALLOCATION_AGENT_KEY) + { + test.jvmArgs(findAllocationJVMArg()) + } + else + { + test.jvmArgs(jvmArg) + } + } + if (categoryConfig.enableAssertions) + { + LogTools.info("Assertions enabled. Adding JVM arg: -ea") + test.enableAssertions = true + } + else + { + LogTools.info("Assertions disabled") + test.enableAssertions = false + } + + test.minHeapSize = "${categoryConfig.minHeapSizeGB}g" + test.maxHeapSize = "${categoryConfig.maxHeapSizeGB}g" + + test.testLogging.info.events = setOf(TestLogEvent.STARTED, + TestLogEvent.FAILED, + TestLogEvent.PASSED, + TestLogEvent.SKIPPED, + TestLogEvent.STANDARD_ERROR, + TestLogEvent.STANDARD_OUT) + + LogTools.info("test.forkEvery = ${test.forkEvery}") + LogTools.info("test.maxParallelForks = ${test.maxParallelForks}") + LogTools.info("test.systemProperties = ${test.systemProperties}") + LogTools.info("test.allJvmArgs = ${test.allJvmArgs}") + LogTools.info("test.minHeapSize = ${test.minHeapSize}") + LogTools.info("test.maxHeapSize = ${test.maxHeapSize}") + + // List tests to be run + LogTools.quiet("Tests to be run:") + testsToTagsMap.value.forEach { entry -> + if ((category == "fast" && entry.value.isEmpty()) || entry.value.contains(category)) + { + LogTools.info(entry.key + " " + entry.value) + } + } + } + + finalizedBy(addPhonyTestXmlTask) + } + } + } + + fun applyCategoryConfigToGradleTest(test: Test, categoryConfig: IHMCCICategory) + { + categoryConfig.doFirst.invoke() + + test.useJUnitPlatform { + for (tag in categoryConfig.includeTags) + { + this.includeTags(tag) + } + for (tag in categoryConfig.excludeTags) + { + this.excludeTags(tag) + } + // If the "fast" category includes nothing, this excludes all tags included by other + // categories, which makes it run only untagged tests and tests that would not be run + // if the user were to run all defined catagories. This is both a safety feature, + // and the expected functionality of the "fast" category, historically at IHMC. + if (categoryConfig.name == "fast" && categoryConfig.includeTags.isEmpty()) + { + for (definedCategory in categoriesExtension.categories) + { + for (tag in definedCategory.value.includeTags) + { + if (tag != "fast") // this allows @Tag("fast") to be used + { + this.excludeTags(tag) + } + } + } + } + } + } + + fun postProcessCategoryConfig(): IHMCCICategory + { + val categoryConfig = categoriesExtension.configure(category) + + if (categoryConfig.name == "fast") // fast runs all "untagged" tests, so exclude all found tags + { + categoryConfig.includeTags.clear() + // https://github.com/junit-team/junit5/issues/1679 + categoryConfig.includeTags.add("fast | none()") + } + minHeapSizeGBOverride.run { if (this is Int) categoryConfig.minHeapSizeGB = this } + maxHeapSizeGBOverride.run { if (this is Int) categoryConfig.maxHeapSizeGB = this } + forkEveryOverride.run { if (this is Int) categoryConfig.forkEvery = this } + maxParallelForksOverride.run { if (this is Int) categoryConfig.maxParallelForks = this } + enableAssertionsOverride.run { if (this is Boolean) categoryConfig.enableAssertions = this } + defaultTimeoutOverride.run { if (this is Int) categoryConfig.defaultTimeout = this } + testTaskTimeoutOverride.run { if (this is Int) categoryConfig.testTaskTimeout = this } + allocationRecordingOverride.run { if (this is Boolean && this) categoryConfig.jvmArguments += ALLOCATION_AGENT_KEY } + + LogTools.info("${categoryConfig.name}.forkEvery = ${categoryConfig.forkEvery}") + LogTools.info("${categoryConfig.name}.maxParallelForks = ${categoryConfig.maxParallelForks}") + LogTools.info("${categoryConfig.name}.excludeTags = ${categoryConfig.excludeTags}") + LogTools.info("${categoryConfig.name}.includeTags = ${categoryConfig.includeTags}") + LogTools.info("${categoryConfig.name}.jvmProperties = ${categoryConfig.jvmProperties}") + LogTools.info("${categoryConfig.name}.jvmArguments = ${categoryConfig.jvmArguments}") + LogTools.info("${categoryConfig.name}.minHeapSizeGB = ${categoryConfig.minHeapSizeGB}") + LogTools.info("${categoryConfig.name}.maxHeapSizeGB = ${categoryConfig.maxHeapSizeGB}") + LogTools.info("${categoryConfig.name}.enableAssertions = ${categoryConfig.enableAssertions}") + LogTools.info("${categoryConfig.name}.defaultTimeout = ${categoryConfig.defaultTimeout}") + LogTools.info("${categoryConfig.name}.testTaskTimeout = ${categoryConfig.testTaskTimeout}") + LogTools.info("${categoryConfig.name}.allocationRecording = ${categoryConfig.jvmArguments}") + + return categoryConfig + } + + fun addPhonyTestXmlTask(anyproject: Project): Task? + { + return anyproject.tasks.create("addPhonyTestXml") { + this.doLast { + var testsFound = false + for (path in anyproject.rootDir.walkBottomUp()) + { + if (path.toPath().toAbsolutePath().toString().matches(Regex(".*/test-results/test/.*\\.xml"))) + { + LogTools.info("Found test file: $path") + testsFound = true + break + } + } + if (!testsFound) + createNoTestsFoundXml(anyproject, anyproject.buildDir.resolve("test-results/test")) + } + } + } + + fun createNoTestsFoundXml(testProject: Project, testDir: File) + { + testProject.mkdir(testDir) + val noTestsFoundFile = testDir.resolve("TEST-us.ihmc.NoTestsFoundTest.xml") + LogTools.info("No tests found. Writing $noTestsFoundFile") + noTestsFoundFile.writeText( + "" + + "" + + "" + + "" + + "This is a phony test to make CI builds pass when a project does not contain any tests." + + "" + + "") + } + + fun findAllocationJVMArg(): String + { + if (allocationJVMArg == null) // search only once + { + for (testProject in testProjects.value) + { + testProject.configurations.getByName("runtimeClasspath").files.forEach { + if (it.name.contains("java-allocation-instrumenter")) + { + allocationJVMArg = "-javaagent:" + it.getAbsolutePath() + LogTools.info("Found allocation JVM arg: $allocationJVMArg") + } + } + } + if (allocationJVMArg == null) // error out, because user needs to add it + { + throw GradleException("[ihmc-ci] Cannot find `java-allocation-instrumenter` on test classpath. Please add it to your test dependencies!") + } + } + + return allocationJVMArg!! + } + + fun loadProperties() + { + project.properties["cpuThreads"].run { if (this != null) cpuThreads = (this as String).toInt() } + project.properties["category"].run { if (this != null) category = (this as String).trim().toLowerCase() } + project.properties["minHeapSizeGB"].run { if (this != null) minHeapSizeGBOverride = (this as String).toInt() } + project.properties["maxHeapSizeGB"].run { if (this != null) maxHeapSizeGBOverride = (this as String).toInt() } + project.properties["forkEvery"].run { if (this != null) forkEveryOverride = (this as String).toInt() } + project.properties["maxParallelForks"].run { if (this != null) maxParallelForksOverride = (this as String).toInt() } + project.properties["enableAssertions"].run { if (this != null) enableAssertionsOverride = (this as String).toBoolean() } + project.properties["defaultTimeout"].run { if (this != null) defaultTimeoutOverride = (this as String).toInt() } + project.properties["testTaskTimeout"].run { if (this != null) testTaskTimeoutOverride = (this as String).toInt() } + project.properties["allocationRecording"].run { if (this != null) allocationRecordingOverride = (this as String).toBoolean() } + LogTools.info("cpuThreads = $cpuThreads") + LogTools.info("category = $category") + LogTools.info("minHeapSizeGB = ${unsetPrintFilter(minHeapSizeGBOverride)}") + LogTools.info("maxHeapSizeGB = ${unsetPrintFilter(maxHeapSizeGBOverride)}") + LogTools.info("forkEvery = ${unsetPrintFilter(forkEveryOverride)}") + LogTools.info("maxParallelForks = ${unsetPrintFilter(maxParallelForksOverride)}") + LogTools.info("enableAssertions = ${unsetPrintFilter(enableAssertionsOverride)}") + LogTools.info("defaultTimeout = ${unsetPrintFilter(defaultTimeoutOverride)}") + LogTools.info("testTaskTimeout = ${unsetPrintFilter(testTaskTimeoutOverride)}") + LogTools.info("allocationRecording = ${unsetPrintFilter(allocationRecordingOverride)}") + } + + private fun unsetPrintFilter(any: Any) = if (any is Unset) "Not set" else any + + fun containsIHMCTestMultiProject(project: Project): Boolean + { + for (allproject in project.allprojects) + { + if (allproject.name.endsWith("-test")) + { + return true + } + } + return false + } +} diff --git a/src/main/kotlin/us/ihmc/ci/JUnitExtension.kt b/src/main/kotlin/us/ihmc/ci/JUnitExtension.kt new file mode 100644 index 00000000..eea3d75f --- /dev/null +++ b/src/main/kotlin/us/ihmc/ci/JUnitExtension.kt @@ -0,0 +1,10 @@ +package us.ihmc.ci + +class JUnitExtension(val jupiterVersion: String, + val platformVersion: String) +{ + fun jupiterApi(): String = "org.junit.jupiter:junit-jupiter-api:$jupiterVersion" + fun jupiterEngine(): String = "org.junit.jupiter:junit-jupiter-engine:$jupiterVersion" + fun platformCommons(): String = "org.junit.platform:junit-platform-commons:$platformVersion" + fun platformLauncher(): String = "org.junit.platform:junit-platform-launcher:$platformVersion" +} \ No newline at end of file diff --git a/src/main/kotlin/us/ihmc/ci/TagParser.kt b/src/main/kotlin/us/ihmc/ci/TagParser.kt new file mode 100644 index 00000000..a57452dc --- /dev/null +++ b/src/main/kotlin/us/ihmc/ci/TagParser.kt @@ -0,0 +1,140 @@ +package us.ihmc.ci + +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginConvention +import org.junit.platform.engine.discovery.ClasspathRootSelector +import org.junit.platform.engine.discovery.DiscoverySelectors +import org.junit.platform.engine.support.descriptor.MethodSource +import org.junit.platform.launcher.LauncherDiscoveryRequest +import org.junit.platform.launcher.TestIdentifier +import org.junit.platform.launcher.TestPlan +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder +import org.junit.platform.launcher.core.LauncherFactory + +import java.io.File +import java.net.URL +import java.net.URLClassLoader +import java.nio.file.Path + +object TagParser +{ + /** + * Return map of fully qualified test names to tag names sourced + * from the JUnit 5 discovery engine itself. + */ + fun parseForTags(testProject: Project, testsToTagsMap: HashMap>) + { + LogTools.info("Discovering tests in $testProject") + + val contextClasspathUrls = arrayListOf() // all of the tests and dependencies + val selectorPaths = hashSetOf() // just the test classes in this project + assembleTestClasspath(testProject, contextClasspathUrls, selectorPaths) + LogTools.debug("Classpath entries: $contextClasspathUrls") + + val originalClassLoader = Thread.currentThread().contextClassLoader + val customClassLoader = URLClassLoader.newInstance(contextClasspathUrls.toTypedArray(), originalClassLoader) + lateinit var testPlan: TestPlan + try + { + Thread.currentThread().contextClassLoader = customClassLoader + debugContextClassLoader(customClassLoader) + + val launcher = LauncherFactory.create() + val builder = LauncherDiscoveryRequestBuilder.request() + builder.selectors(DiscoverySelectors.selectClasspathRoots(selectorPaths)) + builder.configurationParameters(emptyMap()) + val discoveryRequest = builder.build() + debugClasspathSelectors(discoveryRequest) + testPlan = launcher.discover(discoveryRequest) + recursiveBuildMap(testPlan.roots, testPlan, testsToTagsMap) + LogTools.debug("Contains tests: ${testPlan.containsTests()}") + } + finally + { + Thread.currentThread().contextClassLoader = originalClassLoader + } + } + + private fun recursiveBuildMap(set: Set, testPlan: TestPlan, testsToTagsMap: HashMap>) + { + set.forEach { testIdentifier -> + if (testIdentifier.isTest && testIdentifier.source.isPresent && testIdentifier.source.get() is MethodSource) + { + val methodSource = testIdentifier.source.get() as MethodSource + LogTools.debug("Test id: ${testIdentifier.displayName} tags: ${testIdentifier.tags} path: $methodSource") + val fullyQualifiedTestName = methodSource.className + "." + methodSource.methodName + if (!testsToTagsMap.containsKey(fullyQualifiedTestName)) + { + testsToTagsMap.put(fullyQualifiedTestName, hashSetOf()) + } + + testIdentifier.tags.forEach { testTag -> + testsToTagsMap[fullyQualifiedTestName]!!.add(testTag.name) + } + } + else + { + LogTools.debug("Test id: ${testIdentifier.displayName} tags: ${testIdentifier.tags} type: ${testIdentifier.type}") + } + + recursiveBuildMap(testPlan.getChildren(testIdentifier), testPlan, testsToTagsMap) + } + } + + /** + * This function gathers all the paths and JARs comprising the classpath of the test source set + * of the project this plugin is applied to. It is used to simulate conditions as if Gradle or + * JUnit was running those tests. + */ + private fun assembleTestClasspath(testProject: Project, contextClasspathUrls: ArrayList, selectorPaths: HashSet) + { + // TODO: + val java = testProject.convention.getPlugin(JavaPluginConvention::class.java) +// val java = testProject.convention.getPlugin(JavaLibraryPlugin::class.java) +// testProject.plugins. +// testProject.configurations.getByName("default").forEach { file -> + java.sourceSets.getByName("main").compileClasspath.forEach { file -> + addStuffToClasspath(file, contextClasspathUrls, selectorPaths) + } + java.sourceSets.getByName("main").runtimeClasspath.forEach { file -> + addStuffToClasspath(file, contextClasspathUrls, selectorPaths) + } + } + + private fun addStuffToClasspath(file: File, contextClasspathUrls: ArrayList, selectorPaths: HashSet) + { + val entryString = file.toString() + val uri = file.toURI() + val path = file.toPath() + if (entryString.endsWith(".jar")) + { + contextClasspathUrls.add(uri.toURL()) + } + else if (!entryString.endsWith("/")) + { + val fileWithSlash = File("$entryString/") // TODO: Is this necessary? + contextClasspathUrls.add(fileWithSlash.toURI().toURL()) + selectorPaths.add(fileWithSlash.toPath()) + } + else + { + contextClasspathUrls.add(uri.toURL()) + selectorPaths.add(path) + } + } + + private fun debugClasspathSelectors(discoveryRequest: LauncherDiscoveryRequest) + { + discoveryRequest.getSelectorsByType(ClasspathRootSelector::class.java).forEach { + LogTools.debug("Selector: $it") + } + } + + private fun debugContextClassLoader(customClassLoader: URLClassLoader) + { + // make sure context class loader is working + customClassLoader.urLs.forEach { + LogTools.debug(it.toString()) + } + } +} diff --git a/src/test/java/us/ihmc/ci/CategoriesTest.java b/src/test/java/us/ihmc/ci/CategoriesTest.java new file mode 100644 index 00000000..f0099f5b --- /dev/null +++ b/src/test/java/us/ihmc/ci/CategoriesTest.java @@ -0,0 +1,52 @@ +package us.ihmc.ci; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import us.ihmc.build.GradleTestingToolsKt; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +@Tag("categories-test") +public class CategoriesTest +{ + @Test + public void testAllTestsRun() throws IOException + { + String projectName = "categories"; + GradleTestingToolsKt.runGradleTask("-v", projectName); + String cleanOutput = GradleTestingToolsKt.runGradleTask("clean", projectName); + Assertions.assertTrue(cleanOutput.contains("BUILD SUCCESSFUL")); + String output = GradleTestingToolsKt.runGradleTask("test --info -Pcategory=all", projectName); + Assertions.assertTrue(output.contains("BUILD FAILED")); + + System.out.println("Working dir: " + Paths.get(".").toAbsolutePath()); + String results = Files.readString(Paths.get("tests/categories/src/test/build/reports/tests/test/index.html")); + System.out.println(results); + // Asserts 11 tests pass, 1 test fails, 0 tests ignored + Assertions.assertTrue(results.contains( + "us.ihmc.ci" + System.lineSeparator() + "" + System.lineSeparator() + "9" + + System.lineSeparator() + "1" + System.lineSeparator() + "0")); + } + + @Test + public void testFastTestsRun() throws IOException + { + String projectName = "categories"; + GradleTestingToolsKt.runGradleTask("-v", projectName); + String cleanOutput = GradleTestingToolsKt.runGradleTask("clean", projectName); + Assertions.assertTrue(cleanOutput.contains("BUILD SUCCESSFUL")); + String output = GradleTestingToolsKt.runGradleTask("test --info -PincludeTags=fast", projectName); + Assertions.assertTrue(output.contains("BUILD SUCCESSFUL")); + + System.out.println("Working dir: " + Paths.get(".").toAbsolutePath()); + String results = Files.readString(Paths.get("tests/categories/src/test/build/reports/tests/test/index.html")); + System.out.println(results); + // Asserts 5 tests pass, 0 test fails, 0 tests ignored + Assertions.assertTrue(results.contains( + "us.ihmc.ci" + System.lineSeparator() + "" + System.lineSeparator() + "4" + + System.lineSeparator() + "0" + System.lineSeparator() + "0")); + } +} diff --git a/src/test/java/us/ihmc/ci/CategorizedExtendingExtreme.java b/src/test/java/us/ihmc/ci/CategorizedExtendingExtreme.java new file mode 100644 index 00000000..b8029453 --- /dev/null +++ b/src/test/java/us/ihmc/ci/CategorizedExtendingExtreme.java @@ -0,0 +1,5 @@ +package us.ihmc.ci; + +public class CategorizedExtendingExtreme // extends CategorizedAbstractTest +{ +} diff --git a/src/test/java/us/ihmc/ci/CategorizedExtendingTest.java b/src/test/java/us/ihmc/ci/CategorizedExtendingTest.java new file mode 100644 index 00000000..594d164d --- /dev/null +++ b/src/test/java/us/ihmc/ci/CategorizedExtendingTest.java @@ -0,0 +1,12 @@ +package us.ihmc.ci; + +import org.junit.jupiter.api.Test; + +public class CategorizedExtendingTest //extends CategorizedAbstractTest +{ + @Test + public void imAndExtendingTest() + { + //super.imAnAbstractTest(); + } +} diff --git a/src/test/java/us/ihmc/ci/CategorizedTests1.java b/src/test/java/us/ihmc/ci/CategorizedTests1.java new file mode 100644 index 00000000..5de59739 --- /dev/null +++ b/src/test/java/us/ihmc/ci/CategorizedTests1.java @@ -0,0 +1,48 @@ +package us.ihmc.ci; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; + +public class CategorizedTests1 +{ + @Tag("fast") + @Test + public void fastTest() throws InterruptedException + { + } + + @Test + public void untaggedTest() + { + + } + + @Disabled + @Tag("failing") + @Test + public void failingTest() throws InterruptedException + { + Assertions.fail(); + } + + @ResourceLock(value = "File.txt", mode = ResourceAccessMode.READ_WRITE) + @Execution(ExecutionMode.SAME_THREAD) + @Tag("slow") + @Test + public void slowTest() throws InterruptedException + { + } + + @Tag("fast") + @Tag("needs-gpu") + @Test + public void gpuTest() throws InterruptedException + { + } +} diff --git a/src/test/java/us/ihmc/ci/CategorizedTests2.java b/src/test/java/us/ihmc/ci/CategorizedTests2.java new file mode 100644 index 00000000..5873f0ac --- /dev/null +++ b/src/test/java/us/ihmc/ci/CategorizedTests2.java @@ -0,0 +1,31 @@ +package us.ihmc.ci; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +public class CategorizedTests2 +{ + @Tag("fast") + @Test + public void fastTest() throws InterruptedException + { + } + + @Test + public void untaggedTest() throws InterruptedException + { + } + + @Tag("slow") + @Test + public void slowTest() throws InterruptedException + { + } + + @Tag("fast") + @Tag("needs-gpu") + @Test + public void gpuTest() throws InterruptedException + { + } +} diff --git a/src/test/java/us/ihmc/ci/ParallelExecutionTest.java b/src/test/java/us/ihmc/ci/ParallelExecutionTest.java new file mode 100644 index 00000000..bf393f01 --- /dev/null +++ b/src/test/java/us/ihmc/ci/ParallelExecutionTest.java @@ -0,0 +1,50 @@ +package us.ihmc.ci; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import us.ihmc.build.GradleTestingToolsKt; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class ParallelExecutionTest +{ + @Test + public void testAllTestsRun() throws IOException + { + String projectName = "categories"; + GradleTestingToolsKt.runGradleTask("-v", projectName); + String cleanOutput = GradleTestingToolsKt.runGradleTask("clean", projectName); + Assertions.assertTrue(cleanOutput.contains("BUILD SUCCESSFUL")); + String output = GradleTestingToolsKt.runGradleTask("test --info -Pcategory=all", projectName); + Assertions.assertTrue(output.contains("BUILD FAILED")); + + System.out.println("Working dir: " + Paths.get(".").toAbsolutePath()); + String results = new String(Files.readAllBytes(Paths.get("tests/categories/src/test/build/reports/tests/test/index.html"))); + System.out.println(results); + // Asserts 11 tests pass, 1 test fails, 0 tests ignored + Assertions.assertTrue(results.contains( + "us.ihmc.ci" + System.lineSeparator() + "" + System.lineSeparator() + "9" + + System.lineSeparator() + "1" + System.lineSeparator() + "0")); + } + + @Test + public void testFastTestsRun() throws IOException + { + String projectName = "categories"; + GradleTestingToolsKt.runGradleTask("-v", projectName); + String cleanOutput = GradleTestingToolsKt.runGradleTask("clean", projectName); + Assertions.assertTrue(cleanOutput.contains("BUILD SUCCESSFUL")); + String output = GradleTestingToolsKt.runGradleTask("test --info -Pcategory=fast", projectName); + Assertions.assertTrue(output.contains("BUILD SUCCESSFUL")); + + System.out.println("Working dir: " + Paths.get(".").toAbsolutePath()); + String results = new String(Files.readAllBytes(Paths.get("tests/categories/src/test/build/reports/tests/test/index.html"))); + System.out.println(results); + // Asserts 5 tests pass, 0 test fails, 0 tests ignored + Assertions.assertTrue(results.contains( + "us.ihmc.ci" + System.lineSeparator() + "" + System.lineSeparator() + "4" + + System.lineSeparator() + "0" + System.lineSeparator() + "0")); + } +} diff --git a/tests/categories/build.gradle.kts b/tests/categories/build.gradle.kts new file mode 100644 index 00000000..1d463a83 --- /dev/null +++ b/tests/categories/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + id("us.ihmc.ihmc-build") + id("us.ihmc.log-tools-plugin") version "0.6.3" + id("us.ihmc.ihmc-ci") version "7.7" + id("us.ihmc.ihmc-cd") version "1.23" +} + +ihmc { + group = "us.ihmc" + version = "0.1.0" + vcsUrl = "https://your.vcs/url" + openSource = true + + configureDependencyResolution() + configurePublications() +} + +//categories.configure("all") { +// +//} + +//ihmc.sourceSetProject("test").tasks.named("test", Test::class.java) { +// +//} + +mainDependencies { + api("org.apache.commons:commons-lang3:3.12.0") +} + +testDependencies { +} diff --git a/tests/categories/gradle.properties b/tests/categories/gradle.properties new file mode 100644 index 00000000..1d6102c0 --- /dev/null +++ b/tests/categories/gradle.properties @@ -0,0 +1,20 @@ +kebabCasedName = categories +pascalCasedName = Categories +extraSourceSets = ["test"] +publishUrl = local +compositeSearchHeight = 0 +excludeFromCompositeBuild = false +artifactoryUsername = unset_username +artifactoryPassword = unset_password + +# Test suite generator +disableJobCheck = true +crashOnEmptyJobs = false +crashOnMissingTimeouts = true +disableSuiteBalancing = false +maxSuiteDuration = 5.5 +bambooUrl = "https://bamboo.ihmc.us/" +bambooPlanKeys = ["one", "two"] + +includeTags = all +excludeTags = none \ No newline at end of file diff --git a/tests/categories/settings.gradle.kts b/tests/categories/settings.gradle.kts new file mode 100644 index 00000000..58165093 --- /dev/null +++ b/tests/categories/settings.gradle.kts @@ -0,0 +1,20 @@ +pluginManagement { + plugins { + id("us.ihmc.ihmc-build") version "0.29.3" + } +} + +buildscript { + repositories { + maven { url = uri("https://plugins.gradle.org/m2/") } + mavenLocal() + } + dependencies { + classpath("us.ihmc:ihmc-build:0.29.3") + } +} + +val ihmcSettingsConfigurator = us.ihmc.build.IHMCSettingsConfigurator(settings, logger, extra) +ihmcSettingsConfigurator.checkRequiredPropertiesAreSet() +ihmcSettingsConfigurator.configureExtraSourceSets() +ihmcSettingsConfigurator.findAndIncludeCompositeBuilds() diff --git a/tests/categories/src/main/java/us.ihmc/BasicTestApplication.java b/tests/categories/src/main/java/us.ihmc/BasicTestApplication.java new file mode 100644 index 00000000..286bcdcb --- /dev/null +++ b/tests/categories/src/main/java/us.ihmc/BasicTestApplication.java @@ -0,0 +1,9 @@ +package us.ihmc; + +public class BasicTestApplication +{ + public static void main(String[] args) + { + System.out.println("Hello there. I'm some test code."); + } +} diff --git a/tests/categories/src/test/java/us/ihmc/ci/CategorizedAbstractTest.java b/tests/categories/src/test/java/us/ihmc/ci/CategorizedAbstractTest.java new file mode 100644 index 00000000..587cf306 --- /dev/null +++ b/tests/categories/src/test/java/us/ihmc/ci/CategorizedAbstractTest.java @@ -0,0 +1,24 @@ +package us.ihmc.ci; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +public abstract class CategorizedAbstractTest +{ + public void imAnAbstractTest() + { + + } + + @Tag("fast") + @Test + public void someNonExtendedTest() + { + Assertions.assertTimeout(Duration.ofSeconds(30), () -> { + + }); + } +} diff --git a/tests/categories/src/test/java/us/ihmc/ci/CategorizedTests3.java b/tests/categories/src/test/java/us/ihmc/ci/CategorizedTests3.java new file mode 100644 index 00000000..4afa1c76 --- /dev/null +++ b/tests/categories/src/test/java/us/ihmc/ci/CategorizedTests3.java @@ -0,0 +1,46 @@ +package us.ihmc.ci; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; + +public class CategorizedTests3 +{ + @Tag("fast") + @Test + public void fastTest() throws InterruptedException + { + } + + @Test + public void untaggedTest() + { + + } + + @Tag("failing") + @Test + public void failingTest() throws InterruptedException + { + Assertions.fail(); + } + + @ResourceLock(value = "File.txt", mode = ResourceAccessMode.READ_WRITE) + @Execution(ExecutionMode.SAME_THREAD) + @Tag("slow") + @Test + public void slowTest() throws InterruptedException + { + } + + @Tag("fast") + @Tag("needs-gpu") + @Test + public void gpuTest() throws InterruptedException + { + } +} diff --git a/tests/categories/src/test/java/us/ihmc/ci/CategorizedTests4.java b/tests/categories/src/test/java/us/ihmc/ci/CategorizedTests4.java new file mode 100644 index 00000000..244e017d --- /dev/null +++ b/tests/categories/src/test/java/us/ihmc/ci/CategorizedTests4.java @@ -0,0 +1,31 @@ +package us.ihmc.ci; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +public class CategorizedTests4 +{ + @Tag("fast") + @Test + public void fastTest() throws InterruptedException + { + } + + @Test + public void untaggedTest() throws InterruptedException + { + } + + @Tag("slow") + @Test + public void slowTest() throws InterruptedException + { + } + + @Tag("fast") + @Tag("needs-gpu") + @Test + public void gpuTest() throws InterruptedException + { + } +} diff --git a/tests/parallel-execution/build.gradle.kts b/tests/parallel-execution/build.gradle.kts new file mode 100644 index 00000000..fb2fc6ee --- /dev/null +++ b/tests/parallel-execution/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + id("us.ihmc.ihmc-build") + id("us.ihmc.log-tools-plugin") version "0.6.3" + id("us.ihmc.ihmc-ci") version "7.7" + id("us.ihmc.ihmc-cd") version "1.23" +} + +ihmc { + group = "us.ihmc" + version = "0.1.0" + vcsUrl = "https://your.vcs/url" + openSource = true + + configureDependencyResolution() + configurePublications() +} + +ihmc.sourceSetProject("test").tasks.named("test", Test::class.java) { + setForkEvery(1) + maxParallelForks = 20 + + systemProperties["junit.jupiter.execution.parallel.enabled"] = "true" + systemProperties["junit.jupiter.execution.parallel.config.strategy"] = "dynamic" +// systemProperties["junit.jupiter.execution.parallel.config.fixed.parallelism"] = "2" +} + +mainDependencies { + api("org.apache.commons:commons-lang3:3.12.0") +} + +testDependencies { +} diff --git a/tests/parallel-execution/gradle.properties b/tests/parallel-execution/gradle.properties new file mode 100644 index 00000000..1864a592 --- /dev/null +++ b/tests/parallel-execution/gradle.properties @@ -0,0 +1,9 @@ +kebabCasedName = parallel-execution +pascalCasedName = ParallelExecution +extraSourceSets = ["test"] +publishUrl = local +compositeSearchHeight = 0 +excludeFromCompositeBuild = false + +includeTags = all +excludeTags = none \ No newline at end of file diff --git a/tests/parallel-execution/settings.gradle.kts b/tests/parallel-execution/settings.gradle.kts new file mode 100644 index 00000000..58165093 --- /dev/null +++ b/tests/parallel-execution/settings.gradle.kts @@ -0,0 +1,20 @@ +pluginManagement { + plugins { + id("us.ihmc.ihmc-build") version "0.29.3" + } +} + +buildscript { + repositories { + maven { url = uri("https://plugins.gradle.org/m2/") } + mavenLocal() + } + dependencies { + classpath("us.ihmc:ihmc-build:0.29.3") + } +} + +val ihmcSettingsConfigurator = us.ihmc.build.IHMCSettingsConfigurator(settings, logger, extra) +ihmcSettingsConfigurator.checkRequiredPropertiesAreSet() +ihmcSettingsConfigurator.configureExtraSourceSets() +ihmcSettingsConfigurator.findAndIncludeCompositeBuilds() diff --git a/tests/parallel-execution/src/main/java/us.ihmc/BasicTestApplication.java b/tests/parallel-execution/src/main/java/us.ihmc/BasicTestApplication.java new file mode 100644 index 00000000..286bcdcb --- /dev/null +++ b/tests/parallel-execution/src/main/java/us.ihmc/BasicTestApplication.java @@ -0,0 +1,9 @@ +package us.ihmc; + +public class BasicTestApplication +{ + public static void main(String[] args) + { + System.out.println("Hello there. I'm some test code."); + } +} diff --git a/tests/parallel-execution/src/test/java/us/ihmc/ParallelExecution1.java b/tests/parallel-execution/src/test/java/us/ihmc/ParallelExecution1.java new file mode 100644 index 00000000..824358fc --- /dev/null +++ b/tests/parallel-execution/src/test/java/us/ihmc/ParallelExecution1.java @@ -0,0 +1,44 @@ +package us.ihmc; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; + +import java.time.Duration; + +public class ParallelExecution1 +{ + @Tag("fast") + @Test + public void fastTest() throws InterruptedException + { + Thread.sleep(5000); + } + + @Test + public void untaggedTest() throws InterruptedException + { + Thread.sleep(5000); + } + + @ResourceLock(value = "File.txt", mode = ResourceAccessMode.READ_WRITE) + @Execution(ExecutionMode.SAME_THREAD) + @Tag("slow") + @Test + public void slowTest() throws InterruptedException + { + Thread.sleep(5000); + } + + @Tag("fast") + @Tag("needs-gpu") + @Test + public void gpuTest() throws InterruptedException + { + Thread.sleep(5000); + } +} diff --git a/tests/parallel-execution/src/test/java/us/ihmc/ParallelExecution2.java b/tests/parallel-execution/src/test/java/us/ihmc/ParallelExecution2.java new file mode 100644 index 00000000..8bdc9dd6 --- /dev/null +++ b/tests/parallel-execution/src/test/java/us/ihmc/ParallelExecution2.java @@ -0,0 +1,35 @@ +package us.ihmc; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +public class ParallelExecution2 +{ + @Tag("fast") + @Test + public void fastTest() throws InterruptedException + { + Thread.sleep(5000); + } + + @Test + public void untaggedTest() throws InterruptedException + { + Thread.sleep(5000); + } + + @Tag("slow") + @Test + public void slowTest() throws InterruptedException + { + Thread.sleep(5000); + } + + @Tag("fast") + @Tag("needs-gpu") + @Test + public void gpuTest() throws InterruptedException + { + Thread.sleep(5000); + } +}