diff --git a/build.gradle b/build.gradle index efbc2c9b4b67..67fdec4eeda9 100755 --- a/build.gradle +++ b/build.gradle @@ -140,6 +140,35 @@ tasks.register('generate') { // `afterEvaluate.rootProject.generate.dependsOn(generateProto)` } +tasks.register('printTestClasspath') { + group 'Build' + description "Print the classpath used in all tests for all subprojects" + + doLast { + Set result = new LinkedHashSet() + // Prefer sources at the start of the classpath + subprojects.each { sub -> + if (sub.hasProperty("sourceSets")) { + sub.sourceSets.each { ss -> + ss.each { x -> + x.output.classesDirs.each { y -> result.add(y) } + } + } + } + } + + subprojects.each { sub -> + sub.configurations.each { c -> + if (c.name.toLowerCase().endsWith("runtimeclasspath")) { + c.each { f -> result.add(f) } + } + } + } + + println result.join(File.pathSeparator) + } +} + // Prompt the user for a publication passsword to sign archives or upload artifacts, if requested if (project.hasProperty('askpass')) { gradle.taskGraph.whenReady { taskGraph -> diff --git a/ci/pipelines/shared/jinja.variables.yml b/ci/pipelines/shared/jinja.variables.yml index 77ce92921d2b..32585b288397 100644 --- a/ci/pipelines/shared/jinja.variables.yml +++ b/ci/pipelines/shared/jinja.variables.yml @@ -143,7 +143,7 @@ tests: CALL_STACK_TIMEOUT: '20700' CPUS: '96' DUNIT_PARALLEL_FORKS: '24' - EXECUTE_TEST_TIMEOUT: 6h + EXECUTE_TEST_TIMEOUT: 10h GRADLE_TASK: repeatTest PARALLEL_DUNIT: 'true' PARALLEL_GRADLE: 'false' diff --git a/ci/scripts/repeat-new-tests.sh b/ci/scripts/repeat-new-tests.sh index 4a5406256630..f744523294c9 100755 --- a/ci/scripts/repeat-new-tests.sh +++ b/ci/scripts/repeat-new-tests.sh @@ -42,6 +42,19 @@ function changes_for_path() { popd >> /dev/null } +function save_classpath() { + echo "Building and saving classpath" + pushd geode >> /dev/null + # Do this twice since devBuild still dumps a warning string to stdout. + ./gradlew --console=plain -q devBuild 2>/dev/null + ./gradlew --console=plain -q printTestClasspath 2>/dev/null >/tmp/classpath.txt + popd >> /dev/null +} + +function create_gradle_test_targets() { + echo $(${JAVA_HOME}/bin/java -cp $(cat /tmp/classpath.txt) org.apache.geode.test.util.StressNewTestHelper $@) +} + UNIT_TEST_CHANGES=$(changes_for_path '*/src/test/java') || exit $? INTEGRATION_TEST_CHANGES=$(changes_for_path '*/src/integrationTest/java') || exit $? DISTRIBUTED_TEST_CHANGES=$(changes_for_path '*/src/distributedTest/java') || exit $? @@ -51,7 +64,7 @@ UPGRADE_TEST_CHANGES=$(changes_for_path '*/src/upgradeTest/java') || exit $? CHANGED_FILES_ARRAY=( $UNIT_TEST_CHANGES $INTEGRATION_TEST_CHANGES $DISTRIBUTED_TEST_CHANGES $ACCEPTANCE_TEST_CHANGES $UPGRADE_TEST_CHANGES ) NUM_CHANGED_FILES=${#CHANGED_FILES_ARRAY[@]} -echo "${NUM_CHANGED_FILES} changed tests" +echo "${NUM_CHANGED_FILES} changed test files" if [[ "${NUM_CHANGED_FILES}" -eq 0 ]] then @@ -59,36 +72,21 @@ then exit 0 fi -if [[ "${NUM_CHANGED_FILES}" -gt 25 ]] -then - echo "${NUM_CHANGED_FILES} is too many changed tests to stress test. Allowing this job to pass without stress testing." - exit 0 -fi +save_classpath -TEST_TARGETS="" - -function append_to_test_targets() { - local target="$1" - local files="$2" - if [[ -n "$files" ]] - then - TEST_TARGETS="$TEST_TARGETS $target" - for FILENAME in $files - do - SHORT_NAME=$(basename $FILENAME) - SHORT_NAME="${SHORT_NAME%.java}" - TEST_TARGETS="$TEST_TARGETS --tests $SHORT_NAME" - done - fi -} +TEST_TARGETS=$(create_gradle_test_targets ${CHANGED_FILES_ARRAY[@]}) +TEST_COUNT=$(echo ${TEST_TARGETS} | sed -e 's/.*testCount=\([0-9]*\).*/\1/g') -append_to_test_targets "repeatUnitTest" "$UNIT_TEST_CHANGES" -append_to_test_targets "repeatIntegrationTest" "$INTEGRATION_TEST_CHANGES" -append_to_test_targets "repeatDistributedTest" "$DISTRIBUTED_TEST_CHANGES" -append_to_test_targets "repeatUpgradeTest" "$UPGRADE_TEST_CHANGES" +if [[ "${NUM_CHANGED_FILES}" -ne "${TEST_COUNT}" ]] +then + echo "Changed test files increased to ${TEST_COUNT} after including subclasses" +fi -# Acceptance tests cannot currently run in parallel, so do not stress these tests -#append_to_test_targets "repeatAcceptanceTest" "$ACCEPTANCE_TEST_CHANGES" +if [[ "${TEST_COUNT}" -gt 35 ]] +then + echo "${TEST_COUNT} is too many changed tests to stress test. Allowing this job to pass without stress testing." + exit 0 +fi export GRADLE_TASK="compileTestJava compileIntegrationTestJava compileDistributedTestJava $TEST_TARGETS" export GRADLE_TASK_OPTIONS="-Prepeat=50 -PfailOnNoMatchingTests=false" diff --git a/geode-junit/build.gradle b/geode-junit/build.gradle index 5c83d2d8deb5..8f5521139082 100755 --- a/geode-junit/build.gradle +++ b/geode-junit/build.gradle @@ -29,6 +29,7 @@ dependencies { compileOnly(project(':geode-unsafe')) compileOnly(project(':geode-gfsh')) + api('io.github.classgraph:classgraph') compile('com.fasterxml.jackson.core:jackson-annotations') compile('com.fasterxml.jackson.core:jackson-databind') compile('com.github.stefanbirkner:system-rules') { diff --git a/geode-junit/src/main/java/org/apache/geode/test/util/StressNewTestHelper.java b/geode-junit/src/main/java/org/apache/geode/test/util/StressNewTestHelper.java new file mode 100644 index 000000000000..6806f01b6ad6 --- /dev/null +++ b/geode-junit/src/main/java/org/apache/geode/test/util/StressNewTestHelper.java @@ -0,0 +1,197 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package org.apache.geode.test.util; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ClassInfoList; +import io.github.classgraph.ScanResult; + +/** + * This class is intended as a helper to the CI StressNewTest job. Given a list of changed test java + * files, it expands the list to include any subclasses and outputs a partial Gradle command line to + * execute those tests depending on the 'category' of test (unit, distributed, etc.). + */ +public class StressNewTestHelper { + + private ScanResult scanResult; + private String packageToScan; + + // Mapping of source set to list of tests + private Map> sourceToTestMapping = new HashMap<>(); + + private static final Pattern categoryPattern = Pattern.compile(".*/src/(.*?)/java/.*"); + private static final Pattern intellijCategoryPattern = + Pattern.compile(".*/out/test/.*\\.(.*?)/.*"); + private static final Pattern gradleCategoryPattern = + Pattern.compile(".*/build/classes/java/(.*?)/.*"); + + private static final Map sourceToGradleMapping = new HashMap<>(); + + static { + sourceToGradleMapping.put("test", "repeatUnitTest"); + sourceToGradleMapping.put("integrationTest", "repeatIntegrationTest"); + sourceToGradleMapping.put("distributedTest", "repeatDistributedTest"); + sourceToGradleMapping.put("upgradeTest", "repeatUpgradeTest"); + // Cannot currently be run repeatedly because of docker issues + // sourceToGradleMapping.put("acceptanceTest", "repeatAcceptanceTest"); + } + + private static class TestClassInfo { + final String originalFilename; + final String category; + final String className; + final String simpleClassName; + + TestClassInfo(String originalFilename, String category, String className, + String simpleClassName) { + this.originalFilename = originalFilename; + this.category = category; + this.className = className; + this.simpleClassName = simpleClassName; + } + } + + public StressNewTestHelper(String packageToScan) { + this.packageToScan = packageToScan; + scanResult = new ClassGraph().whitelistPackages(packageToScan) + .enableClassInfo() + .enableAnnotationInfo().scan(); + } + + public String buildGradleCommand() { + StringBuilder command = new StringBuilder(); + + int testCount = 0; + for (Map.Entry> entry : sourceToTestMapping.entrySet()) { + String sourceSet = entry.getKey(); + if (sourceToGradleMapping.get(sourceSet) == null) { + System.err.println("Skipping repeat test for " + sourceSet); + continue; + } + + command.append(sourceToGradleMapping.get(sourceSet)); + command.append(" --tests "); + command.append(String.join(",", entry.getValue())); + command.append(" "); + testCount += entry.getValue().size(); + } + + // This exists so that scripts processing this output can extract the number of tests + // included here. Yes, it's pretty hacky... + command.append("-PtestCount=" + testCount); + + return command.toString(); + } + + public void add(String javaFile) { + TestClassInfo testClassInfo = createTestClassInfo(javaFile); + List extenders = whatExtends(testClassInfo); + + if (!scanResult.getClassInfo(testClassInfo.className).isAbstract()) { + extenders.add(testClassInfo); + } + + if (extenders.isEmpty()) { + addTestToCategory(testClassInfo.category, testClassInfo.simpleClassName); + return; + } + + extenders.forEach(e -> addTestToCategory(e.category, e.simpleClassName)); + } + + private void addTestToCategory(String category, String testClass) { + Set listOfTests = sourceToTestMapping.computeIfAbsent(category, k -> new TreeSet<>()); + listOfTests.add(testClass); + } + + private List whatExtends(TestClassInfo testClass) { + List results = new ArrayList<>(); + ClassInfoList subClasses = scanResult.getSubclasses(testClass.className); + + for (ClassInfo classInfo : subClasses) { + String classFilename = classInfo.getClasspathElementURL().getFile(); + results.add( + new TestClassInfo(classFilename, getCategory(classFilename), classInfo.getName(), + classInfo.getSimpleName())); + } + + return results; + } + + private TestClassInfo createTestClassInfo(String javaFile) { + String category = getCategory(javaFile); + String sanitized = javaFile.replace("/", "."); + + int packageStart = sanitized.indexOf(packageToScan); + if (packageStart >= 0) { + sanitized = sanitized.substring(packageStart); + } + + if (sanitized.endsWith(".java")) { + int javaIdx = sanitized.indexOf(".java"); + sanitized = sanitized.substring(0, javaIdx); + } + + int classIndex = sanitized.lastIndexOf("."); + + return new TestClassInfo(javaFile, category, sanitized, sanitized.substring(classIndex + 1)); + } + + private String getCategory(String javaFile) { + Matcher matcher = categoryPattern.matcher(javaFile); + + // Maybe we're running tests in Intellij + if (!matcher.matches()) { + matcher = intellijCategoryPattern.matcher(javaFile); + } + + // Maybe we're running tests in Gradle + if (!matcher.matches()) { + matcher = gradleCategoryPattern.matcher(javaFile); + } + + if (!matcher.matches()) { + throw new IllegalArgumentException("Unable to determine category for " + javaFile); + } + + return matcher.group(1); + } + + public static void main(String[] args) { + StressNewTestHelper helper = new StressNewTestHelper("org.apache.geode"); + + for (String arg : args) { + try { + helper.add(arg); + } catch (Exception e) { + System.err.println("ERROR: Unable to process " + arg + " : " + e.getMessage()); + } + } + + System.out.println(helper.buildGradleCommand()); + } + +} diff --git a/geode-junit/src/test/java/org/apache/geode/test/util/WhatExtendsJUnitTest.java b/geode-junit/src/test/java/org/apache/geode/test/util/WhatExtendsJUnitTest.java new file mode 100644 index 000000000000..ea59a03bbc76 --- /dev/null +++ b/geode-junit/src/test/java/org/apache/geode/test/util/WhatExtendsJUnitTest.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package org.apache.geode.test.util; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import org.junit.Before; +import org.junit.Test; + +public class WhatExtendsJUnitTest { + + private StressNewTestHelper scanner; + + public abstract static class A { + } + + public static class B extends A { + } + + public static class C extends B { + } + + @Before + public void setup() { + scanner = new StressNewTestHelper("org.apache.geode"); + } + + @Test + public void nothingExtendsC() { + scanner.add(getClassLocation(C.class)); + assertThat(scanner.buildGradleCommand()) + .isEqualTo("repeatUnitTest --tests WhatExtendsJUnitTest$C -PtestCount=1"); + } + + @Test + public void classAisExtendedByBandC() { + scanner.add(getClassLocation(A.class)); + assertThat(scanner.buildGradleCommand()).isEqualTo( + "repeatUnitTest --tests WhatExtendsJUnitTest$B,WhatExtendsJUnitTest$C -PtestCount=2"); + } + + @Test + public void classAisExtendedByBandC_withDuplicatesRemoved() { + scanner.add(getClassLocation(A.class)); + scanner.add(getClassLocation(A.class)); + assertThat(scanner.buildGradleCommand()) + .isEqualTo( + "repeatUnitTest --tests WhatExtendsJUnitTest$B,WhatExtendsJUnitTest$C -PtestCount=2"); + } + + @Test + public void usingJavaFileWithSameCategoryAsSubClasses() { + scanner.add(getClassLocation(A.class, "foo/src/test/java/")); + assertThat(scanner.buildGradleCommand()) + .isEqualTo( + "repeatUnitTest --tests WhatExtendsJUnitTest$B,WhatExtendsJUnitTest$C -PtestCount=2"); + } + + @Test + public void usingJavaFileWithDifferentCategoryAsSubClasses() { + scanner.add(getClassLocation(A.class, "foo/src/integrationTest/java/")); + scanner.add(getClassLocation(this.getClass(), "foo/src/integrationTest/java/")); + assertThat(scanner.buildGradleCommand()) + .isEqualTo( + "repeatUnitTest --tests WhatExtendsJUnitTest$B,WhatExtendsJUnitTest$C repeatIntegrationTest --tests WhatExtendsJUnitTest -PtestCount=3"); + } + + @Test + public void ignoreAcceptanceTestSourcesForNow() { + scanner.add(getClassLocation(this.getClass(), "foo/src/acceptanceTest/java/")); + assertThat(scanner.buildGradleCommand()).isEqualTo("-PtestCount=0"); + } + + private String getClassLocation(Class clazz) { + String codeSource = clazz.getProtectionDomain().getCodeSource().getLocation().getFile(); + String classFile = clazz.getName().replace(".", "/"); + + return codeSource + classFile; + } + + private String getClassLocation(Class clazz, String fakePrefix) { + String classFile = clazz.getName().replace(".", "/"); + + return fakePrefix + classFile; + } +} diff --git a/geode-junit/src/test/resources/expected-pom.xml b/geode-junit/src/test/resources/expected-pom.xml index ca38f34dbe30..b90a0dedac93 100644 --- a/geode-junit/src/test/resources/expected-pom.xml +++ b/geode-junit/src/test/resources/expected-pom.xml @@ -46,6 +46,11 @@ + + io.github.classgraph + classgraph + compile + com.fasterxml.jackson.core jackson-annotations