diff --git a/.idea/modules.xml b/.idea/modules.xml index 049379723553..bb30ded81900 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -43,6 +43,7 @@ + diff --git a/platform/build-scripts/groovy/org/jetbrains/intellij/build/TestingOptions.groovy b/platform/build-scripts/groovy/org/jetbrains/intellij/build/TestingOptions.groovy index 897175707c9b..9339db332d4a 100644 --- a/platform/build-scripts/groovy/org/jetbrains/intellij/build/TestingOptions.groovy +++ b/platform/build-scripts/groovy/org/jetbrains/intellij/build/TestingOptions.groovy @@ -31,10 +31,17 @@ class TestingOptions { String testGroups = System.getProperty("intellij.build.test.groups", OLD_TEST_GROUP) /** - * Semicolon-separated patterns for test class names which need to be executed. Wildcard '*' is supported. + * Semicolon-separated patterns for test class names which need to be executed. Wildcard '*' is supported. If this option is specified, + * {@link #testGroups} will be ignored. */ String testPatterns = System.getProperty("intellij.build.test.patterns", OLD_TEST_PATTERNS) + /** + * Semicolon-separated names of JUnit run configurations in the project which need to be executed. If this option is specified, + * {@link #testGroups} and {@link #testPatterns} will be ignored. + */ + String testConfigurations = System.getProperty("intellij.build.test.configurations") + /** * Specifies components from which product will be used to run tests, by default IDEA Ultimate will be used. */ @@ -70,7 +77,8 @@ class TestingOptions { */ String customJrePath = System.getProperty("intellij.build.test.jre") - private static final String OLD_TEST_GROUP = System.getProperty("idea.test.group", "ALL_EXCLUDE_DEFINED") + public static final String ALL_EXCLUDE_DEFINED_GROUP = "ALL_EXCLUDE_DEFINED" + private static final String OLD_TEST_GROUP = System.getProperty("idea.test.group", ALL_EXCLUDE_DEFINED_GROUP) private static final String OLD_TEST_PATTERNS = System.getProperty("idea.test.patterns") private static final String OLD_PLATFORM_PREFIX = System.getProperty("idea.platform.prefix") private static final int OLD_DEBUG_PORT = SystemProperties.getIntProperty("debug.port", -1) diff --git a/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/JUnitRunConfigurationProperties.groovy b/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/JUnitRunConfigurationProperties.groovy new file mode 100644 index 000000000000..86fa6bda1c2e --- /dev/null +++ b/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/JUnitRunConfigurationProperties.groovy @@ -0,0 +1,81 @@ +/* + * Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ +package org.jetbrains.intellij.build.impl + +import com.intellij.openapi.util.io.FileUtil +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import org.jetbrains.intellij.build.BuildMessages + +@CompileStatic +class JUnitRunConfigurationProperties { + final String name + final String moduleName + final List testClassPatterns + final List vmParameters + final List requiredArtifacts + + static JUnitRunConfigurationProperties findRunConfiguration(String projectHome, String name, BuildMessages messages) { + def file = new File(projectHome, ".idea/runConfigurations/${FileUtil.sanitizeFileName(name)}.xml") + if (!file.exists()) { + messages.error("Cannot find run configurations: $file doesn't exist") + } + + loadRunConfiguration(file, messages) + } + + @SuppressWarnings(["GrUnresolvedAccess", "GroovyAssignabilityCheck"]) + @CompileDynamic + static JUnitRunConfigurationProperties loadRunConfiguration(File file, BuildMessages messages) { + def root = new XmlParser().parse(file) + def configuration = first(root.configuration) + if (configuration == null) { + messages.error("Cannot load configuration from '$file.name': 'configuration' tag is not found") + } + if (configuration.@type != "JUnit") { + messages.error("Cannot load configuration from '$file.name': only JUnit run configuration are supported") + } + + String moduleName = first(configuration.module)?.@name + if (moduleName == null) { + messages.error("Cannot run configuration from '$file.name': module name is not specified") + } + Map options = configuration.option?.collectEntries { [it.@name, it.@value] } + def testKind = options["TEST_OBJECT"] + List testClassPatterns + if (testKind == "class") { + testClassPatterns = [options["MAIN_CLASS_NAME"]] + } + else if (testKind == "package") { + testClassPatterns = [options["PACKAGE_NAME"] + ".*"] + } + else if (testKind == "pattern") { + testClassPatterns = first(configuration?.patterns)?.pattern?.collect { it.@testClass } + } + else { + messages.error("Cannot run $file.name configuration: '${testKind}' test kind is not supported") + testClassPatterns = [] + } + + List requiredArtifacts = + first(configuration.method)?.option?. + find { it.@name == "BuildArtifacts" && it.@enabled == "true" }?. + artifact?.collect { it.@name } ?: [] + + def vmParameters = options["VM_PARAMETERS"].tokenize() + return new JUnitRunConfigurationProperties(configuration.@name, moduleName, testClassPatterns, vmParameters, requiredArtifacts) + } + + private static T first(Collection collection) { + collection == null || collection.isEmpty() ? null : collection.first() + } + + JUnitRunConfigurationProperties(String name, String moduleName, List testClassPatterns, List vmParameters, List requiredArtifacts) { + this.name = name + this.moduleName = moduleName + this.testClassPatterns = testClassPatterns + this.requiredArtifacts = requiredArtifacts + this.vmParameters = vmParameters + } +} diff --git a/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/TestingTasksImpl.groovy b/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/TestingTasksImpl.groovy index 1114678c694f..49ee02534aa3 100644 --- a/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/TestingTasksImpl.groovy +++ b/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/TestingTasksImpl.groovy @@ -18,7 +18,6 @@ package org.jetbrains.intellij.build.impl import com.intellij.execution.CommandLineWrapperUtil import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.FileUtilRt -import com.intellij.openapi.util.text.StringUtil import groovy.transform.CompileDynamic import groovy.transform.CompileStatic import org.apache.tools.ant.AntClassLoader @@ -32,7 +31,6 @@ import org.jetbrains.jps.util.JpsPathUtil import java.util.function.Predicate import java.util.jar.Manifest - /** * @author nik */ @@ -48,16 +46,126 @@ class TestingTasksImpl extends TestingTasks { @Override void runTests(List additionalJvmOptions, String defaultMainModule, Predicate rootExcludeCondition) { + checkOptions() + def compilationTasks = CompilationTasks.create(context) - if (options.mainModule != null) { + def runConfigurations = options.testConfigurations?.split(";")?.collect { String name -> + JUnitRunConfigurationProperties.findRunConfiguration(context.paths.projectHome, name, context.messages) + } + if (runConfigurations != null) { + compilationTasks.compileModules(["tests_bootstrap"], ["platform-build-scripts"] + runConfigurations.collect { it.moduleName }) + compilationTasks.buildProjectArtifacts(runConfigurations.collectMany {it.requiredArtifacts}) + } + else if (options.mainModule != null) { compilationTasks.compileModules(["tests_bootstrap"], [options.mainModule, "platform-build-scripts"]) } else { compilationTasks.compileAllModulesAndTests() } + setupTestingDependencies() + def remoteDebugJvmOptions = System.getProperty("teamcity.remote-debug.jvm.options") + if (remoteDebugJvmOptions != null) { + debugTests(remoteDebugJvmOptions, additionalJvmOptions, defaultMainModule, rootExcludeCondition) + } + else if (runConfigurations != null) { + runTestsFromRunConfigurations(additionalJvmOptions, runConfigurations) + } + else { + runTestsFromGroupsAndPatterns(additionalJvmOptions, defaultMainModule, rootExcludeCondition) + } + } + + private void checkOptions() { + if (options.testConfigurations != null) { + if (options.testPatterns != null) { + context.messages.warning("'intellij.build.test.configurations' option is specified so 'intellij.build.test.patterns' will be ignored.") + } + if (options.testGroups != TestingOptions.ALL_EXCLUDE_DEFINED_GROUP) { + context.messages.warning("'intellij.build.test.configurations' option is specified so 'intellij.build.test.groups' will be ignored.") + } + if (options.testConfigurations != null && options.mainModule != null) { + context.messages.warning("'intellij.build.test.configurations' option is specified so 'intellij.build.test.main.module' will be ignored.") + } + } + else if (options.testPatterns != null && options.testGroups != TestingOptions.ALL_EXCLUDE_DEFINED_GROUP) { + context.messages.warning("'intellij.build.test.patterns' option is specified so 'intellij.build.test.groups' will be ignored.") + } + } + + private void runTestsFromRunConfigurations(List additionalJvmOptions, List runConfigurations) { + runConfigurations.each { configuration -> + context.messages.block("Run '${configuration.name}' run configuration") { + runTestsFromRunConfiguration(configuration, additionalJvmOptions) + } + } + } + + private void runTestsFromRunConfiguration(JUnitRunConfigurationProperties runConfigurationProperties, List additionalJvmOptions) { + context.messages.progress("Running '${runConfigurationProperties.name}' run configuration") + List filteredVmOptions = removeStandardJvmOptions(runConfigurationProperties.vmParameters) + runTestsProcess(runConfigurationProperties.moduleName, null, runConfigurationProperties.testClassPatterns.join(";"), + filteredVmOptions + additionalJvmOptions, [:], false) + } + + private static List removeStandardJvmOptions(List vmOptions) { + def ignoredPrefixes = [ + "-ea", "-XX:+HeapDumpOnOutOfMemoryError", + "-Xbootclasspath", + "-Xmx", "-Xms", + "-Didea.system.path=", "-Didea.config.path=", "-Didea.home.path=" + ] + vmOptions.findAll { option -> ignoredPrefixes.every { !option.startsWith(it) } } + } + + private void runTestsFromGroupsAndPatterns(List additionalJvmOptions, String defaultMainModule, Predicate rootExcludeCondition) { + Map additionalSystemProperties = [:] def mainModule = options.mainModule ?: defaultMainModule + if (rootExcludeCondition != null) { + List excludedModules = context.project.modules.findAll { + List contentRoots = it.contentRootsList.urls + !contentRoots.isEmpty() && rootExcludeCondition.test(JpsPathUtil.urlToFile(contentRoots.first())) + } + List excludedRoots = excludedModules.collectMany { + [context.getModuleOutputPath(it), context.getModuleTestsOutputPath(it)] + } + File excludedRootsFile = new File("$context.paths.temp/excluded.classpath") + excludedRootsFile.text = excludedRoots.findAll { new File(it).exists() }.join('\n') + additionalSystemProperties["exclude.tests.roots.file"] = excludedRootsFile.absolutePath + } + + runTestsProcess(mainModule, options.testGroups, options.testPatterns, additionalJvmOptions, additionalSystemProperties, false + ) + } + + private void debugTests(String remoteDebugJvmOptions, List additionalJvmOptions, String defaultMainModule, Predicate rootExcludeCondition) { + def testConfigurationType = System.getProperty("teamcity.remote-debug.type") + if (testConfigurationType != "junit") { + context.messages.error("Remote debugging is supported for junit run configurations only, but 'teamcity.remote-debug.type' is $testConfigurationType") + } + + def testObject = System.getProperty("teamcity.remote-debug.junit.type") + def junitClass = System.getProperty("teamcity.remote-debug.junit.class") + if (testObject != "class") { + context.messages.error("Remote debugging supports debugging all test methods in a class for now, debugging isn't supported for '$testObject'") + } + if (junitClass == null) { + context.messages.error("Remote debugging supports debugging all test methods in a class for now, but target class isn't specified") + } + if (options.testPatterns != null) { + context.messages.warning("'intellij.build.test.patterns' option is ignored while debugging via TeamCity plugin") + } + if (options.testConfigurations != null) { + context.messages.warning("'intellij.build.test.configurations' option is ignored while debugging via TeamCity plugin") + } + def mainModule = options.mainModule ?: defaultMainModule + def filteredOptions = removeStandardJvmOptions(remoteDebugJvmOptions.split(";").toList()) + runTestsProcess(mainModule, null, junitClass, filteredOptions + additionalJvmOptions, [:], true) + } + + private void runTestsProcess(String mainModule, String testGroups, String testPatterns, + List additionalJvmOptions, Map additionalSystemProperties, boolean remoteDebugging) { List testsClasspath = context.getModuleRuntimeClasspath(context.findRequiredModule(mainModule), true) List bootstrapClasspath = context.getModuleRuntimeClasspath(context.findRequiredModule("tests_bootstrap"), false) def classpathFile = new File("$context.paths.temp/junit.classpath") @@ -91,35 +199,14 @@ class TestingTasksImpl extends TestingTasks { String tempDir = System.getProperty("teamcity.build.tempDir", System.getProperty("java.io.tmpdir")) - def remoteDebugJvmOptions = System.getProperty("teamcity.remote-debug.jvm.options") - if (remoteDebugJvmOptions != null) { - def testConfigurationType = System.getProperty("teamcity.remote-debug.type") - if (testConfigurationType != "junit") { - context.messages.error("Remote debugging is supported for junit run configurations only, but 'teamcity.remote-debug.type' is $testConfigurationType") - } - - def testObject = System.getProperty("teamcity.remote-debug.junit.type") - def junitClass = System.getProperty("teamcity.remote-debug.junit.class") - if (testObject != "class") { - context.messages.error("Remote debugging supports debugging all test methods in a class for now, debugging isn't supported for '$testObject'") - } - if (junitClass == null) { - context.messages.error("Remote debugging supports debugging all test methods in a class for now, but target class isn't specified") - } - if (options.testPatterns != null) { - context.messages.warning("'intellij.build.test.patterns' option is ignored while debugging via TeamCity plugin") - } - options.testPatterns = junitClass - } - Map systemProperties = [ "classpath.file" : classpathFile.absolutePath, "idea.platform.prefix" : options.platformPrefix, "idea.home.path" : context.paths.projectHome, "idea.config.path" : "$tempDir/config".toString(), "idea.system.path" : "$tempDir/system".toString(), - "intellij.build.test.patterns" : options.testPatterns, - "intellij.build.test.groups" : options.testGroups, + "intellij.build.test.patterns" : testPatterns, + "intellij.build.test.groups" : testGroups, "idea.performance.tests" : System.getProperty("idea.performance.tests"), "idea.coverage.enabled.build" : System.getProperty("idea.coverage.enabled.build"), "teamcity.buildConfName" : System.getProperty("teamcity.buildConfName"), @@ -131,6 +218,7 @@ class TestingTasksImpl extends TestingTasks { "file.encoding" : "UTF-8", "io.netty.leakDetectionLevel" : "PARANOID", ] as Map + systemProperties.putAll(additionalSystemProperties) (System.getProperties() as Hashtable).each { String key, String value -> if (key.startsWith("pass.")) { @@ -138,33 +226,12 @@ class TestingTasksImpl extends TestingTasks { } } - if (rootExcludeCondition != null) { - List excludedModules = context.project.modules.findAll { - List contentRoots = it.contentRootsList.urls - !contentRoots.isEmpty() && rootExcludeCondition.test(JpsPathUtil.urlToFile(contentRoots.first())) - } - List excludedRoots = excludedModules.collectMany { - [context.getModuleOutputPath(it), context.getModuleTestsOutputPath(it)] - } - File excludedRootsFile = new File("$context.paths.temp/excluded.classpath") - excludedRootsFile.text = excludedRoots.findAll { new File(it).exists() }.join('\n') - systemProperties["exclude.tests.roots.file"] = excludedRootsFile.absolutePath - } - boolean suspendDebugProcess = options.suspendDebugProcess if (systemProperties["idea.performance.tests"] == "true") { context.messages.info("Debugging disabled for performance tests") suspendDebugProcess = false } - else if (remoteDebugJvmOptions != null) { - // ignore all options from the run configuration because they may conflict with the options defined in the build script - String jvmDebugOption = StringUtil.splitHonorQuotes(remoteDebugJvmOptions, ' ' as char).find { it.startsWith("-agentlib:jdwp=") } - if (jvmDebugOption == null) { - context.messages.error("Cannot extract JVM debugging options from $remoteDebugJvmOptions") - } - - jvmArgs.add(jvmDebugOption) - + else if (remoteDebugging) { context.messages.info("Remote debugging via TeamCity plugin is activated.") if (suspendDebugProcess) { context.messages.warning("'intellij.build.test.debug.suspend' option is ignored while debugging via TeamCity plugin") @@ -179,7 +246,7 @@ class TestingTasksImpl extends TestingTasks { jvmArgs.add(debuggerParameter) } - context.messages.info("Starting ${options.testGroups != null ? "test from groups '$options.testGroups'" : "all tests"}") + context.messages.info("Starting ${testGroups != null ? "test from groups '${testGroups}'" : "all tests"}") if (options.customJrePath != null) { context.messages.info("JVM: $options.customJrePath") } diff --git a/platform/build-scripts/tests/platform-build-scripts-tests.iml b/platform/build-scripts/tests/platform-build-scripts-tests.iml new file mode 100644 index 000000000000..ddb12af2d8c7 --- /dev/null +++ b/platform/build-scripts/tests/platform-build-scripts-tests.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/build-scripts/tests/testResources/org/jetbrains/intellij/build/runConfigurations/test_class.xml b/platform/build-scripts/tests/testResources/org/jetbrains/intellij/build/runConfigurations/test_class.xml new file mode 100644 index 000000000000..bd86250ae070 --- /dev/null +++ b/platform/build-scripts/tests/testResources/org/jetbrains/intellij/build/runConfigurations/test_class.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/platform/build-scripts/tests/testResources/org/jetbrains/intellij/build/runConfigurations/test_package.xml b/platform/build-scripts/tests/testResources/org/jetbrains/intellij/build/runConfigurations/test_package.xml new file mode 100644 index 000000000000..59a633310af0 --- /dev/null +++ b/platform/build-scripts/tests/testResources/org/jetbrains/intellij/build/runConfigurations/test_package.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/platform/build-scripts/tests/testResources/org/jetbrains/intellij/build/runConfigurations/test_pattern.xml b/platform/build-scripts/tests/testResources/org/jetbrains/intellij/build/runConfigurations/test_pattern.xml new file mode 100644 index 000000000000..c30153749057 --- /dev/null +++ b/platform/build-scripts/tests/testResources/org/jetbrains/intellij/build/runConfigurations/test_pattern.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/build-scripts/tests/testSrc/org/jetbrains/intellij/build/JUnitRunConfigurationPropertiesTest.kt b/platform/build-scripts/tests/testSrc/org/jetbrains/intellij/build/JUnitRunConfigurationPropertiesTest.kt new file mode 100644 index 000000000000..a9e9f5b9dc2e --- /dev/null +++ b/platform/build-scripts/tests/testSrc/org/jetbrains/intellij/build/JUnitRunConfigurationPropertiesTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ +package org.jetbrains.intellij.build + +import com.intellij.util.io.URLUtil +import org.jetbrains.intellij.build.impl.JUnitRunConfigurationProperties +import org.junit.Assert.assertEquals +import org.junit.Test + +class JUnitRunConfigurationPropertiesTest { + @Test + fun `load test class`() { + val properties = loadRunConfiguration("test_class.xml") + assertEquals("test class", properties.name) + assertEquals("main-module", properties.moduleName) + assertEquals(listOf("com.example.test.MainTest"), properties.testClassPatterns) + assertEquals(listOf("-ea", "-Xmx512m"), properties.vmParameters) + assertEquals(listOf("artifact1", "artifact2"), properties.requiredArtifacts) + } + + @Test + fun `load test package`() { + val properties = loadRunConfiguration("test_package.xml") + assertEquals("test package", properties.name) + assertEquals("main-module", properties.moduleName) + assertEquals(listOf("com.example.test.*"), properties.testClassPatterns) + assertEquals(listOf("-ea"), properties.vmParameters) + assertEquals(emptyList(), properties.requiredArtifacts) + } + + @Test + fun `load test pattern`() { + val properties = loadRunConfiguration("test_pattern.xml") + assertEquals("test pattern", properties.name) + assertEquals("main-module", properties.moduleName) + assertEquals(listOf("com.example.Test", "com.example.package..*"), properties.testClassPatterns) + assertEquals(listOf("-ea"), properties.vmParameters) + assertEquals(emptyList(), properties.requiredArtifacts) + } + + private fun loadRunConfiguration(fileName: String): JUnitRunConfigurationProperties { + val url = JUnitRunConfigurationPropertiesTest::class.java.getResource("runConfigurations/$fileName") + return JUnitRunConfigurationProperties.loadRunConfiguration(URLUtil.urlToFile(url), MockBuildMessages()) + } +} \ No newline at end of file diff --git a/platform/build-scripts/tests/testSrc/org/jetbrains/intellij/build/MockBuildMessages.kt b/platform/build-scripts/tests/testSrc/org/jetbrains/intellij/build/MockBuildMessages.kt new file mode 100644 index 000000000000..5c8fc530a0fb --- /dev/null +++ b/platform/build-scripts/tests/testSrc/org/jetbrains/intellij/build/MockBuildMessages.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ +package org.jetbrains.intellij.build + +import groovy.lang.Closure +import junit.framework.AssertionFailedError + +class MockBuildMessages : BuildMessages { + override fun info(message: String?) { + } + + override fun warning(message: String?) { + } + + override fun error(message: String?) { + throw AssertionFailedError(message) + } + + override fun error(message: String?, cause: Throwable?) { + throw AssertionFailedError(message) + } + + override fun compilationError(compilerName: String?, message: String?) { + } + + override fun compilationErrors(compilerName: String?, messages: MutableList?) { + } + + override fun progress(message: String?) { + } + + override fun block(blockName: String, body: Closure): V { + return body.call() + } + + override fun artifactBuilt(relativeArtifactPath: String?) { + } + + override fun reportStatisticValue(key: String?, value: String?) { + } + + override fun forkForParallelTask(taskName: String?): BuildMessages { + throw UnsupportedOperationException() + } + + override fun onAllForksFinished() { + } + + override fun onForkStarted() { + } + + override fun onForkFinished() { + } +} \ No newline at end of file