Skip to content

Commit

Permalink
GEODE-8603: Potentially expand classes identified for CI stressing to…
Browse files Browse the repository at this point in the history
… include subclasses (#5601) (#5674)

- Make StressNewTestHelper create the complete gradle test task commands
- Since some tests may have subclasses in different source sets, (which
  would require a different repeat task name), it's easier for the
  command generation to all happen in the java helper rather than a
  combination of bash and java.
- Include candidate test class if it is not abstract
- Output a fake Gradle param so that scripts can determine the number of
  tests included.
- Change the CI stress job timeout from 6 to 10 hours.
- Increase the test count threshold from 25 to 35 changed tests. This
  number also includes any tests inferred by this new code.
  • Loading branch information
jdeppe-pivotal committed Oct 27, 2020
1 parent 38ed638 commit 4039a36
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 30 deletions.
29 changes: 29 additions & 0 deletions build.gradle
Expand Up @@ -134,6 +134,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 ->
Expand Down
2 changes: 1 addition & 1 deletion ci/pipelines/shared/jinja.variables.yml
Expand Up @@ -157,7 +157,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'
Expand Down
54 changes: 26 additions & 28 deletions ci/scripts/repeat-new-tests.sh
Expand Up @@ -46,6 +46,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 $?
Expand All @@ -55,44 +68,29 @@ 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
echo "No changed test files, nothing to test."
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"
Expand Down
2 changes: 1 addition & 1 deletion geode-junit/build.gradle
Expand Up @@ -37,7 +37,7 @@ dependencies {
api('org.assertj:assertj-core')
api('org.mockito:mockito-core')


api('io.github.classgraph:classgraph')
api('com.fasterxml.jackson.core:jackson-annotations')
api('com.fasterxml.jackson.core:jackson-databind')
api('com.github.stefanbirkner:system-rules') {
Expand Down
@@ -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<String, Set<String>> 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<String, String> 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<String, Set<String>> 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<TestClassInfo> 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<String> listOfTests = sourceToTestMapping.computeIfAbsent(category, k -> new TreeSet<>());
listOfTests.add(testClass);
}

private List<TestClassInfo> whatExtends(TestClassInfo testClass) {
List<TestClassInfo> 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());
}

}

0 comments on commit 4039a36

Please sign in to comment.