Rework testing conventions gradle plugin (#87213)

This PR reworks the testing conventions precommit plugin. This plugin now:
- is compatible with yaml, java rest tests and internalClusterTest (aka different sourceSets per test type)
- enforces test base class and simple naming conventions (as it did before)
- adds one check task per test sourceSet
- uses the worker api to improve task execution parallelism and encapsulation
- is gradle configuration cache compatible  

This also ports the TestingConventions integration testing to Spock and removes the build-tools-internal/test kit folder that is not required anymore. We also add some common logic for testing java related gradle plugins. 
We will apply further cleanup on other tests within our test suite in a dedicated follow up cleanup
This commit is contained in:
Rene Groeschke 2022-06-20 16:26:38 +02:00 committed by GitHub
parent 52e2e374e8
commit cdf5bd7ed0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 877 additions and 1245 deletions

View File

@ -213,6 +213,7 @@ repositories {
configurations {
integTestRuntimeOnly.extendsFrom(testRuntimeOnly)
}
dependencies {
constraints {
integTestImplementation('org.ow2.asm:asm:9.3')
@ -297,7 +298,6 @@ tasks.named('test').configure {
useJUnitPlatform()
}
tasks.register("integTest", Test) {
inputs.dir(file("src/testKit")).withPropertyName("testkit dir").withPathSensitivity(PathSensitivity.RELATIVE)
systemProperty 'test.version_under_test', version
testClassesDirs = sourceSets.integTest.output.classesDirs
classpath = sourceSets.integTest.runtimeClasspath

View File

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.fixtures
import org.elasticsearch.gradle.internal.conventions.precommit.PrecommitPlugin
abstract class AbstractGradlePrecommitPluginFuncTest extends AbstractJavaGradleFuncTest {
abstract <T extends PrecommitPlugin> Class<T> getPluginClassUnderTest();
def setup() {
buildFile << """
import ${getPluginClassUnderTest().getName()}
plugins {
// bring in build-tools-internal onto the classpath
id 'elasticsearch.global-build-info'
}
// internally used plugins do not have a plugin id as they are
// not intended to be used directly from build scripts
plugins.apply(${getPluginClassUnderTest().getSimpleName()})
"""
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.fixtures
import net.bytebuddy.ByteBuddy
import net.bytebuddy.dynamic.DynamicType
import org.junit.rules.ExternalResource
import org.junit.rules.TemporaryFolder
class LocalRepositoryFixture extends ExternalResource{
private TemporaryFolder temporaryFolder
LocalRepositoryFixture(TemporaryFolder temporaryFolder){
this.temporaryFolder = temporaryFolder
}
void generateJar(String group, String module, String version, String... clazzNames){
def baseGroupFolderPath = group.replace('.', '/')
def targetFolder = new File(repoDir, "${baseGroupFolderPath}/$module/$version")
targetFolder.mkdirs()
def jarFile = new File(targetFolder, "${module}-${version}.jar")
clazzNames.each {clazzName ->
DynamicType.Unloaded<?> dynamicType = new ByteBuddy().subclass(Object.class)
.name(clazzName)
.make()
if(jarFile.exists()) {
dynamicType.inject(jarFile);
}else {
dynamicType.toJar(jarFile);
}
}
}
void configureBuild(File buildFile) {
buildFile << """
repositories {
maven {
name = "local-test"
url = "${getRepoDir()}"
metadataSources {
artifact()
}
}
}
"""
}
File getRepoDir() {
new File(temporaryFolder.root, 'local-repo')
}
}

View File

@ -24,7 +24,7 @@ class ElasticsearchJavaModulePathPluginFuncTest extends AbstractJavaGradleFuncTe
public static final String ES_VERSION = VersionProperties.getElasticsearch()
def setup() {
javaMainClass()
clazz("org.acme.JavaMainClass")
subProject("some-lib") << """
plugins {
id 'java-library'

View File

@ -8,19 +8,22 @@
package org.elasticsearch.gradle.internal.precommit
import org.elasticsearch.gradle.fixtures.AbstractGradleFuncTest
import org.elasticsearch.gradle.fixtures.AbstractGradlePrecommitPluginFuncTest
import org.elasticsearch.gradle.internal.conventions.precommit.LicenseHeadersPrecommitPlugin
import org.elasticsearch.gradle.internal.conventions.precommit.PrecommitPlugin
import org.gradle.testkit.runner.TaskOutcome
class LicenseHeadersPrecommitPluginFuncTest extends AbstractGradleFuncTest {
class LicenseHeadersPrecommitPluginFuncTest extends AbstractGradlePrecommitPluginFuncTest {
Class<? extends PrecommitPlugin> pluginClassUnderTest = LicenseHeadersPrecommitPlugin.class
def setup() {
buildFile << """
apply plugin:'java'
"""
}
def "detects invalid files with invalid license header"() {
given:
buildFile << """
plugins {
id 'java'
id 'elasticsearch.internal-licenseheaders'
}
"""
dualLicensedFile()
unknownSourceFile()
unapprovedSourceFile()
@ -39,11 +42,6 @@ class LicenseHeadersPrecommitPluginFuncTest extends AbstractGradleFuncTest {
def "can filter source files"() {
given:
buildFile << """
plugins {
id 'java'
id 'elasticsearch.internal-licenseheaders'
}
tasks.named("licenseHeaders").configure {
excludes << 'org/acme/filtered/**/*'
}
@ -61,12 +59,6 @@ class LicenseHeadersPrecommitPluginFuncTest extends AbstractGradleFuncTest {
def "supports sspl by convention"() {
given:
buildFile << """
plugins {
id 'java'
id 'elasticsearch.internal-licenseheaders'
}
"""
dualLicensedFile()
when:
@ -79,11 +71,6 @@ class LicenseHeadersPrecommitPluginFuncTest extends AbstractGradleFuncTest {
def "sspl default additional license can be overridden"() {
given:
buildFile << """
plugins {
id 'java'
id 'elasticsearch.internal-licenseheaders'
}
tasks.named("licenseHeaders").configure {
additionalLicense 'ELAST', 'Elastic License 2.0', '2.0; you may not use this file except in compliance with the Elastic License'
}
@ -173,4 +160,5 @@ class LicenseHeadersPrecommitPluginFuncTest extends AbstractGradleFuncTest {
String normalizedPath = normalized(sourceFile.getPath())
(normalizedPath.substring(normalizedPath.indexOf("src/main/java")) - "src/main/java/" - ("/" + sourceFile.getName())).replaceAll("/", ".")
}
}

View File

@ -0,0 +1,295 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.internal.precommit
import org.elasticsearch.gradle.fixtures.AbstractGradlePrecommitPluginFuncTest
import org.elasticsearch.gradle.fixtures.LocalRepositoryFixture
import org.elasticsearch.gradle.internal.conventions.precommit.PrecommitPlugin
import org.gradle.testkit.runner.TaskOutcome
import org.junit.ClassRule
import org.junit.rules.TemporaryFolder
import spock.lang.Shared
import spock.lang.Unroll
class TestingConventionsPrecommitPluginFuncTest extends AbstractGradlePrecommitPluginFuncTest {
Class<? extends PrecommitPlugin> pluginClassUnderTest = TestingConventionsPrecommitPlugin.class
@ClassRule
@Shared
public TemporaryFolder repoFolder = new TemporaryFolder()
@Shared
@ClassRule
public LocalRepositoryFixture repository = new LocalRepositoryFixture(repoFolder)
def setupSpec() {
repository.generateJar('org.apache.lucene', 'tests.util', "1.0",
"org.apache.lucene.tests.util.LuceneTestCase",
"org.elasticsearch.test.ESSingleNodeTestCase",
"org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase",
"org.elasticsearch.test.AbstractMultiClustersTestCase"
)
repository.generateJar('org.junit', 'junit', "4.42",
"org.junit.Assert", "org.junit.Test"
)
}
def setup() {
repository.configureBuild(buildFile)
}
def "skips convention check if no tests available"() {
given:
buildFile << """
apply plugin:'java'
"""
when:
def result = gradleRunner("precommit").build()
then:
result.task(":testTestingConventions").outcome == TaskOutcome.NO_SOURCE
result.task(":testingConventions").outcome == TaskOutcome.UP_TO_DATE
}
def "testing convention tasks are cacheable and uptodate"() {
given:
simpleJavaBuild()
testClazz("org.acme.valid.SomeTests", "org.apache.lucene.tests.util.LuceneTestCase") {
"""
public void testMe() {
}
"""
}
when:
gradleRunner("clean", "precommit", "--build-cache").build()
def result = gradleRunner("clean", "precommit", "--build-cache").build()
then:
result.task(":testTestingConventions").outcome == TaskOutcome.FROM_CACHE
result.task(":testingConventions").outcome == TaskOutcome.UP_TO_DATE
when:
result = gradleRunner("precommit").build()
then:
result.task(":testTestingConventions").outcome == TaskOutcome.UP_TO_DATE
result.task(":testingConventions").outcome == TaskOutcome.UP_TO_DATE
}
def "testing convention plugin is configuration cache compatible"() {
given:
simpleJavaBuild()
testClazz("org.acme.valid.SomeTests", "org.apache.lucene.tests.util.LuceneTestCase") {
"""
public void testMe() {
}
"""
}
when:
def result = gradleRunner("precommit", "--configuration-cache").build()
then:
assertOutputContains(result.getOutput(), "0 problems were found storing the configuration cache.")
when:
result = gradleRunner("precommit", "--configuration-cache").build()
then:
assertOutputContains(result.getOutput(), "Configuration cache entry reused.")
}
def "checks base class convention"() {
given:
simpleJavaBuild()
testClazz("org.acme.valid.SomeTests", "org.apache.lucene.tests.util.LuceneTestCase") {
"""
public void testMe() {
}
"""
}
when:
def result = gradleRunner("precommit").build()
then:
result.task(":testTestingConventions").outcome == TaskOutcome.SUCCESS
result.task(":testingConventions").outcome == TaskOutcome.SUCCESS
when:
testClazz("org.acme.InvalidTests") {
"""
public void testMe() {
}
"""
}
result = gradleRunner("precommit").buildAndFail()
then:
result.task(":testTestingConventions").outcome == TaskOutcome.FAILED
assertOutputContains(result.getOutput(), """\
* What went wrong:
Execution failed for task ':testTestingConventions'.
> A failure occurred while executing org.elasticsearch.gradle.internal.precommit.TestingConventionsCheckTask\$TestingConventionsCheckWorkAction
> Following test classes do not extend any supported base class:
\torg.acme.InvalidTests""".stripIndent()
)
}
def "checks naming convention"() {
given:
simpleJavaBuild()
buildFile << """
tasks.named('testTestingConventions').configure {
suffix 'UnitTest'
}
"""
testClazz("org.acme.valid.SomeNameMissmatchingTest", "org.apache.lucene.tests.util.LuceneTestCase") {
"""
public void testMe() {
}
"""
}
testClazz("org.acme.valid.SomeMatchingUnitTest", "org.apache.lucene.tests.util.LuceneTestCase") {
"""
public void testMe() {
}
"""
}
when:
def result = gradleRunner("precommit").buildAndFail()
then:
result.task(":testTestingConventions").outcome == TaskOutcome.FAILED
assertOutputContains(result.getOutput(), """\
* What went wrong:
Execution failed for task ':testTestingConventions'.
> A failure occurred while executing org.elasticsearch.gradle.internal.precommit.TestingConventionsCheckTask\$TestingConventionsCheckWorkAction
> Following test classes do not match naming convention to use suffix 'UnitTest':
\torg.acme.valid.SomeNameMissmatchingTest""".stripIndent()
)
}
def "provided base classes do not need match naming convention"() {
given:
simpleJavaBuild()
buildFile << """
tasks.named('testTestingConventions').configure {
baseClass 'org.acme.SomeCustomTestBaseClass'
}
"""
testClazz("org.acme.SomeCustomTestBaseClass", "org.junit.Assert")
testClazz("org.acme.valid.SomeNameMatchingTests", "org.acme.SomeCustomTestBaseClass") {
"""
public void testMe() {
}
"""
}
when:
def result = gradleRunner("precommit").build()
then:
result.task(":testTestingConventions").outcome == TaskOutcome.SUCCESS
}
def "applies conventions on yaml-rest-test tests"() {
given:
clazz(dir('src/yamlRestTest/java'), "org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase")
buildFile << """
apply plugin:'elasticsearch.internal-yaml-rest-test'
dependencies {
yamlRestTestImplementation "org.apache.lucene:tests.util:1.0"
yamlRestTestImplementation "org.junit:junit:4.42"
}
"""
clazz(dir("src/yamlRestTest/java"), "org.acme.valid.SomeMatchingIT", "org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase") {
"""
public void testMe() {
}
"""
}
clazz(dir("src/yamlRestTest/java"), "org.acme.valid.SomeOtherMatchingIT", null) {
"""
public void testMe() {
}
"""
}
when:
def result = gradleRunner("testingConventions").buildAndFail()
then:
result.task(":yamlRestTestTestingConventions").outcome == TaskOutcome.FAILED
assertOutputContains(result.getOutput(), """\
* What went wrong:
Execution failed for task ':yamlRestTestTestingConventions'.
> A failure occurred while executing org.elasticsearch.gradle.internal.precommit.TestingConventionsCheckTask\$TestingConventionsCheckWorkAction
> Following test classes do not extend any supported base class:
\torg.acme.valid.SomeOtherMatchingIT""".stripIndent()
)
}
@Unroll
def "applies conventions on #sourceSetName tests"() {
given:
clazz(dir("src/${sourceSetName}/java"), "org.elasticsearch.test.ESIntegTestCase")
clazz(dir("src/${sourceSetName}/java"), "org.elasticsearch.test.rest.ESRestTestCase")
buildFile << """
import org.elasticsearch.gradle.internal.precommit.TestingConventionsCheckTask
apply plugin:'$pluginName'
dependencies {
${sourceSetName}Implementation "org.apache.lucene:tests.util:1.0"
${sourceSetName}Implementation "org.junit:junit:4.42"
}
tasks.withType(TestingConventionsCheckTask).configureEach {
suffix 'IT'
suffix 'Tests'
}
"""
clazz(dir("src/${sourceSetName}/java"), "org.acme.valid.SomeMatchingIT", "org.elasticsearch.test.ESIntegTestCase") {
"""
public void testMe() {}
"""
}
clazz(dir("src/${sourceSetName}/java"), "org.acme.valid.SomeNonMatchingTest", "org.elasticsearch.test.ESIntegTestCase") {
"""
public void testMe() {}
"""
}
when:
def result = gradleRunner("testingConventions").buildAndFail()
then:
result.task(taskName).outcome == TaskOutcome.FAILED
assertOutputContains(result.getOutput(), """\
* What went wrong:
Execution failed for task '${taskName}'.
> A failure occurred while executing org.elasticsearch.gradle.internal.precommit.TestingConventionsCheckTask\$TestingConventionsCheckWorkAction
> Following test classes do not match naming convention to use suffix 'IT' or 'Tests':
\torg.acme.valid.SomeNonMatchingTest""".stripIndent()
)
where:
pluginName | taskName | sourceSetName
"elasticsearch.internal-java-rest-test" | ":javaRestTestTestingConventions" | "javaRestTest"
"elasticsearch.internal-cluster-test" | ":internalClusterTestTestingConventions" | "internalClusterTest"
}
private void simpleJavaBuild() {
buildFile << """
apply plugin:'java'
dependencies {
testImplementation "org.apache.lucene:tests.util:1.0"
testImplementation "org.junit:junit:4.42"
}
"""
}
}

View File

@ -1,105 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.jarhell;
import org.elasticsearch.gradle.internal.test.GradleIntegrationTestCase;
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.GradleRunner;
public class TestingConventionsTasksIT extends GradleIntegrationTestCase {
@Override
public String projectName() {
return "testingConventions";
}
public void testInnerClasses() {
GradleRunner runner = getGradleRunner().withArguments("clean", ":no_tests_in_inner_classes:testingConventions", "-i", "-s");
BuildResult result = runner.buildAndFail();
assertOutputContains(
result.getOutput(),
"Test classes implemented by inner classes will not run:",
" * org.elasticsearch.gradle.testkit.NastyInnerClasses$LooksLikeATestWithoutNamingConvention1",
" * org.elasticsearch.gradle.testkit.NastyInnerClasses$LooksLikeATestWithoutNamingConvention2",
" * org.elasticsearch.gradle.testkit.NastyInnerClasses$LooksLikeATestWithoutNamingConvention3",
" * org.elasticsearch.gradle.testkit.NastyInnerClasses$NamingConventionIT",
" * org.elasticsearch.gradle.testkit.NastyInnerClasses$NamingConventionTests"
);
}
public void testNamingConvention() {
GradleRunner runner = getGradleRunner().withArguments("clean", ":incorrect_naming_conventions:testingConventions", "-i", "-s");
BuildResult result = runner.buildAndFail();
assertOutputContains(
result.getOutput(),
"Seem like test classes but don't match naming convention:",
" * org.elasticsearch.gradle.testkit.LooksLikeATestWithoutNamingConvention1",
" * org.elasticsearch.gradle.testkit.LooksLikeATestWithoutNamingConvention2",
" * org.elasticsearch.gradle.testkit.LooksLikeATestWithoutNamingConvention3"
);
assertOutputMissing(result.getOutput(), "LooksLikeTestsButAbstract");
}
public void testNoEmptyTasks() {
GradleRunner runner = getGradleRunner().withArguments("clean", ":empty_test_task:testingConventions", "-i", "-s");
BuildResult result = runner.buildAndFail();
assertOutputContains(
result.getOutput(),
"Expected at least one test class included in task :empty_test_task:emptyTest, but found none.",
"Expected at least one test class included in task :empty_test_task:test, but found none."
);
}
public void testAllTestTasksIncluded() {
GradleRunner runner = getGradleRunner().withArguments("clean", ":all_classes_in_tasks:testingConventions", "-i", "-s");
BuildResult result = runner.buildAndFail();
assertOutputContains(
result.getOutput(),
"Test classes are not included in any enabled task (:all_classes_in_tasks:test):",
" * org.elasticsearch.gradle.testkit.NamingConventionIT"
);
}
public void testTaskNotImplementBaseClass() {
GradleRunner runner = getGradleRunner().withArguments("clean", ":not_implementing_base:testingConventions", "-i", "-s");
BuildResult result = runner.buildAndFail();
assertOutputContains(
result.getOutput(),
"Tests classes with suffix `IT` should extend org.elasticsearch.gradle.testkit.Integration but the following classes do not:",
" * org.elasticsearch.gradle.testkit.NamingConventionIT",
" * org.elasticsearch.gradle.testkit.NamingConventionMissmatchIT",
"Tests classes with suffix `Tests` should extend org.elasticsearch.gradle.testkit.Unit but the following classes do not:",
" * org.elasticsearch.gradle.testkit.NamingConventionMissmatchTests",
" * org.elasticsearch.gradle.testkit.NamingConventionTests"
);
}
public void testValidSetupWithoutBaseClass() {
GradleRunner runner = getGradleRunner().withArguments("clean", ":valid_setup_no_base:testingConventions", "-i", "-s");
BuildResult result = runner.build();
assertTaskSuccessful(result, ":valid_setup_no_base:testingConventions");
}
public void testValidSetupWithBaseClass() {
GradleRunner runner = getGradleRunner().withArguments("clean", ":valid_setup_with_base:testingConventions", "-i", "-s");
BuildResult result = runner.build();
assertTaskSuccessful(result, ":valid_setup_with_base:testingConventions");
}
public void testTestsInMain() {
GradleRunner runner = getGradleRunner().withArguments("clean", ":tests_in_main:testingConventions", "-i", "-s");
BuildResult result = runner.buildAndFail();
assertOutputContains(
result.getOutput(),
"Classes matching the test naming convention should be in test not main:",
" * NamingConventionIT",
" * NamingConventionTests"
);
}
}

View File

@ -8,8 +8,6 @@
package org.elasticsearch.gradle.internal;
import org.elasticsearch.gradle.internal.precommit.TestingConventionsPrecommitPlugin;
import org.elasticsearch.gradle.internal.precommit.TestingConventionsTasks;
import org.gradle.api.Project;
public class InternalPluginBuildPlugin implements InternalPlugin {
@ -17,23 +15,5 @@ public class InternalPluginBuildPlugin implements InternalPlugin {
public void apply(Project project) {
project.getPluginManager().apply(BuildPlugin.class);
project.getPluginManager().apply(BaseInternalPluginBuildPlugin.class);
project.getPlugins()
.withType(
TestingConventionsPrecommitPlugin.class,
plugin -> project.getTasks().withType(TestingConventionsTasks.class).named("testingConventions").configure(t -> {
t.getNaming().clear();
t.getNaming()
.create(
"Tests",
testingConventionRule -> testingConventionRule.baseClass("org.apache.lucene.tests.util.LuceneTestCase")
);
t.getNaming().create("IT", testingConventionRule -> {
testingConventionRule.baseClass("org.elasticsearch.test.ESIntegTestCase");
testingConventionRule.baseClass("org.elasticsearch.test.rest.ESRestTestCase");
testingConventionRule.baseClass("org.elasticsearch.test.ESSingleNodeTestCase");
});
})
);
}
}

View File

@ -26,17 +26,7 @@ public class InternalPrecommitTasks {
project.getPluginManager().apply(LicenseHeadersPrecommitPlugin.class);
project.getPluginManager().apply(FilePermissionsPrecommitPlugin.class);
project.getPluginManager().apply(LoggerUsagePrecommitPlugin.class);
// TestingConventionsPlugin is incompatible with projects without
// test source sets. We wanna remove this plugin once we moved away from
// StandaloneRestTest plugin and RestTestPlugin. For apply this plugin only
// when tests are available.
// Long Term we want remove the need for most of this plugins functionality
// and not rely on multiple test tasks against a sourceSet.
if (project.file("src/test").exists()) {
project.getPluginManager().apply(TestingConventionsPrecommitPlugin.class);
}
project.getPluginManager().apply(TestingConventionsPrecommitPlugin.class);
// tasks with just tests don't need certain tasks to run, so this flag makes adding
// the task optional
if (withProductiveCode) {

View File

@ -1,89 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.internal.precommit;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Represent rules for tests enforced by the @{link {@link TestingConventionsTasks}}
*
* Rules are identified by name, tests must have this name as a suffix and implement one of the base classes
* and be part of all the specified tasks.
*/
public class TestingConventionRule implements Serializable {
private final String suffix;
private Set<String> baseClasses = new HashSet<>();
private Set<Pattern> taskNames = new HashSet<>();
public TestingConventionRule(String suffix) {
this.suffix = suffix;
}
public String getSuffix() {
return suffix;
}
/**
* Alias for @{link getSuffix} as Gradle requires a name property
*
*/
public String getName() {
return suffix;
}
public void baseClass(String clazz) {
baseClasses.add(clazz);
}
public void setBaseClasses(Collection<String> baseClasses) {
this.baseClasses.clear();
this.baseClasses.addAll(baseClasses);
}
public void taskName(Pattern expression) {
taskNames.add(expression);
}
public void taskName(String expression) {
taskNames.add(Pattern.compile(expression));
}
public void setTaskNames(Collection<Pattern> expressions) {
taskNames.clear();
taskNames.addAll(expressions);
}
public Set<String> getBaseClasses() {
return baseClasses;
}
public Set<Pattern> getTaskNames() {
return taskNames;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestingConventionRule that = (TestingConventionRule) o;
return Objects.equals(suffix, that.suffix);
}
@Override
public int hashCode() {
return Objects.hash(suffix);
}
}

View File

@ -0,0 +1,255 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.internal.precommit;
import org.elasticsearch.gradle.internal.conventions.precommit.PrecommitTask;
import org.gradle.api.GradleException;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.EmptyFileVisitor;
import org.gradle.api.file.FileTree;
import org.gradle.api.file.FileVisitDetails;
import org.gradle.api.logging.Logging;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.IgnoreEmptyDirectories;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.SkipWhenEmpty;
import org.gradle.api.tasks.TaskAction;
import org.gradle.workers.WorkAction;
import org.gradle.workers.WorkParameters;
import org.gradle.workers.WorkQueue;
import org.gradle.workers.WorkerExecutor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.inject.Inject;
@CacheableTask
public abstract class TestingConventionsCheckTask extends PrecommitTask {
@Input
abstract ListProperty<String> getSuffixes();
@Internal
abstract ConfigurableFileCollection getTestClassesDirs();
@InputFiles
@SkipWhenEmpty
@IgnoreEmptyDirectories
@PathSensitive(PathSensitivity.RELATIVE)
public FileTree getTestClasses() {
return getTestClassesDirs().getAsFileTree().matching(pattern -> pattern.include("**/*.class"));
}
@Classpath
abstract ConfigurableFileCollection getClasspath();
@Input
public abstract ListProperty<String> getBaseClasses();
@Inject
public abstract WorkerExecutor getWorkerExecutor();
public void baseClass(String qualifiedClassname) {
getBaseClasses().add(qualifiedClassname);
}
public void suffix(String suffix) {
getSuffixes().add(suffix);
}
@TaskAction
void validate() {
WorkQueue workQueue = getWorkerExecutor().classLoaderIsolation(spec -> spec.getClasspath().from(getClasspath()));
workQueue.submit(TestingConventionsCheckWorkAction.class, parameters -> {
parameters.getClasspath().setFrom(getClasspath());
parameters.getClassDirectories().setFrom(getTestClassesDirs());
parameters.getBaseClassesNames().set(getBaseClasses().get());
parameters.getSuffixes().set(getSuffixes().get());
});
}
abstract static class TestingConventionsCheckWorkAction implements WorkAction<Parameters> {
private static final String JUNIT3_TEST_METHOD_PREFIX = "test";
private static final Predicate<Class<?>> isAbstractClass = clazz -> Modifier.isAbstract(clazz.getModifiers());
private static final Predicate<Class<?>> isPublicClass = clazz -> Modifier.isPublic(clazz.getModifiers());
private static final Predicate<Class<?>> isStaticClass = clazz -> Modifier.isStatic(clazz.getModifiers());
private static final Predicate<Class<?>> testClassDefaultPredicate = isAbstractClass.negate()
.and(isPublicClass)
.and(isStaticClass.negate());
@Inject
public TestingConventionsCheckWorkAction() {}
@Override
public void execute() {
ClassLoadingFileVisitor fileVisitor = new ClassLoadingFileVisitor();
getParameters().getClassDirectories().getAsFileTree().visit(fileVisitor);
checkTestClasses(
fileVisitor.getTestClassCandidates(),
getParameters().getBaseClassesNames().get(),
getParameters().getSuffixes().get()
);
}
private void checkTestClasses(List<String> testClassesCandidates, List<String> baseClassNames, List<String> suffixes) {
var testClassCandidates = testClassesCandidates.stream()
.map(className -> loadClassWithoutInitializing(className, getClass().getClassLoader()))
.collect(Collectors.toCollection(ArrayList::new));
var baseClasses = baseClassNames.stream()
.map(className -> loadClassWithoutInitializing(className, getClass().getClassLoader()))
.toList();
testClassCandidates.removeAll(baseClasses);
var matchingBaseClass = getBaseClassMatching(testClassCandidates, baseClasses);
assertMatchesSuffix(suffixes, matchingBaseClass);
testClassCandidates.removeAll(matchingBaseClass);
assertNoMissmatchingTest(testClassCandidates);
}
private void assertNoMissmatchingTest(List<? extends Class<?>> testClassesCandidate) {
var mismatchingBaseClasses = testClassesCandidate.stream()
.filter(testClassDefaultPredicate)
.filter(TestingConventionsCheckWorkAction::seemsLikeATest)
.collect(Collectors.toList());
if (mismatchingBaseClasses.isEmpty() == false) {
throw new GradleException(
"Following test classes do not extend any supported base class:\n\t"
+ mismatchingBaseClasses.stream().map(c -> c.getName()).collect(Collectors.joining("\n\t"))
);
}
}
private void assertMatchesSuffix(List<String> suffixes, List<Class> matchingBaseClass) {
// ensure base class matching do match suffix
var matchingBaseClassNotMatchingSuffix = matchingBaseClass.stream()
.filter(c -> suffixes.stream().allMatch(s -> c.getName().endsWith(s) == false))
.collect(Collectors.toList());
if (matchingBaseClassNotMatchingSuffix.isEmpty() == false) {
throw new GradleException(
"Following test classes do not match naming convention to use suffix "
+ suffixes.stream().map(s -> "'" + s + "'").collect(Collectors.joining(" or "))
+ ":\n\t"
+ matchingBaseClassNotMatchingSuffix.stream().map(c -> c.getName()).collect(Collectors.joining("\n\t"))
);
}
}
private List<Class> getBaseClassMatching(List<? extends Class<?>> testClassCandidates, List<? extends Class<?>> baseClasses) {
Predicate<Class<?>> extendsBaseClass = clazz -> baseClasses.stream().anyMatch(baseClass -> baseClass.isAssignableFrom(clazz));
return testClassCandidates.stream()
.filter(testClassDefaultPredicate)
.filter(extendsBaseClass)
.filter(TestingConventionsCheckWorkAction::seemsLikeATest)
.collect(Collectors.toList());
}
private static boolean seemsLikeATest(Class<?> clazz) {
try {
Class<?> junitTest = loadClassWithoutInitializing("org.junit.Assert", clazz.getClassLoader());
if (junitTest.isAssignableFrom(clazz)) {
Logging.getLogger(TestingConventionsCheckWorkAction.class)
.debug("{} is a test because it extends {}", clazz.getName(), junitTest.getName());
return true;
}
Class<?> junitAnnotation = loadClassWithoutInitializing("org.junit.Test", clazz.getClassLoader());
for (Method method : clazz.getMethods()) {
if (matchesTestMethodNamingConvention(method)) {
Logging.getLogger(TestingConventionsCheckWorkAction.class)
.debug("{} is a test because it has method named '{}'", clazz.getName(), method.getName());
return true;
}
if (isAnnotated(method, junitAnnotation)) {
Logging.getLogger(TestingConventionsCheckWorkAction.class)
.debug(
"{} is a test because it has method '{}' annotated with '{}'",
clazz.getName(),
method.getName(),
junitAnnotation.getName()
);
return true;
}
}
return false;
} catch (NoClassDefFoundError e) {
// Include the message to get more info to get more a more useful message when running Gradle without -s
throw new IllegalStateException("Failed to inspect class " + clazz.getName() + ". Missing class? " + e.getMessage(), e);
}
}
private static boolean matchesTestMethodNamingConvention(Method method) {
return method.getName().startsWith(JUNIT3_TEST_METHOD_PREFIX)
&& Modifier.isStatic(method.getModifiers()) == false
&& method.getReturnType().equals(Void.TYPE);
}
private static boolean isAnnotated(Method method, Class<?> annotation) {
return List.of(method.getAnnotations())
.stream()
.anyMatch(presentAnnotation -> annotation.isAssignableFrom(presentAnnotation.getClass()));
}
private static Class<?> loadClassWithoutInitializing(String name, ClassLoader classLoader) {
try {
return Class.forName(
name,
// Don't initialize the class to save time. Not needed for this test and this doesn't share a VM with any other tests.
false,
classLoader
);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Failed to load class " + name + ". Incorrect classpath?", e);
}
}
}
private static final class ClassLoadingFileVisitor extends EmptyFileVisitor {
private static final String CLASS_POSTFIX = ".class";
private List<String> fullQualifiedClassNames = new ArrayList<>();
@Override
public void visitFile(FileVisitDetails fileVisitDetails) {
String fileName = fileVisitDetails.getName();
if (fileName.endsWith(CLASS_POSTFIX)) {
String packageName = Arrays.stream(fileVisitDetails.getRelativePath().getSegments())
.takeWhile(s -> s.equals(fileName) == false)
.collect(Collectors.joining("."));
String simpleClassName = fileName.replace(CLASS_POSTFIX, "");
String fullQualifiedClassName = packageName + (packageName.isEmpty() ? "" : ".") + simpleClassName;
fullQualifiedClassNames.add(fullQualifiedClassName);
}
}
public List<String> getTestClassCandidates() {
return fullQualifiedClassNames;
}
}
interface Parameters extends WorkParameters {
ConfigurableFileCollection getClassDirectories();
ConfigurableFileCollection getClasspath();
ListProperty<String> getSuffixes();
ListProperty<String> getBaseClassesNames();
}
}

View File

@ -8,41 +8,96 @@
package org.elasticsearch.gradle.internal.precommit;
import org.elasticsearch.gradle.internal.InternalPlugin;
import org.elasticsearch.gradle.internal.conventions.precommit.PrecommitPlugin;
import org.elasticsearch.gradle.util.GradleUtils;
import org.elasticsearch.gradle.internal.test.InternalClusterTestPlugin;
import org.elasticsearch.gradle.internal.test.rest.InternalJavaRestTestPlugin;
import org.elasticsearch.gradle.internal.test.rest.InternalYamlRestTestPlugin;
import org.gradle.api.Action;
import org.gradle.api.NamedDomainObjectProvider;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.tasks.SourceSetContainer;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.api.tasks.testing.Test;
import java.util.stream.Collectors;
import java.util.List;
public class TestingConventionsPrecommitPlugin extends PrecommitPlugin {
public static final String TESTING_CONVENTIONS_TASK_NAME = "testingConventions";
public class TestingConventionsPrecommitPlugin extends PrecommitPlugin implements InternalPlugin {
@Override
public TaskProvider<? extends Task> createTask(Project project) {
TaskProvider<TestingConventionsTasks> testingConventions = project.getTasks()
.register("testingConventions", TestingConventionsTasks.class, t -> {
TestingConventionRule testsRule = t.getNaming().maybeCreate("Tests");
testsRule.baseClass("org.apache.lucene.tests.util.LuceneTestCase");
TestingConventionRule itRule = t.getNaming().maybeCreate("IT");
itRule.baseClass("org.elasticsearch.test.ESIntegTestCase");
itRule.baseClass("org.elasticsearch.test.rest.ESRestTestCase");
t.setCandidateClassFilesProvider(
project.provider(
() -> project.getTasks()
.withType(Test.class)
.matching(Task::getEnabled)
.stream()
.collect(Collectors.toMap(Task::getPath, task -> task.getCandidateClassFiles().getFiles()))
)
);
SourceSetContainer javaSourceSets = GradleUtils.getJavaSourceSets(project);
t.setSourceSets(javaSourceSets);
// Run only after everything is compiled
javaSourceSets.all(sourceSet -> t.dependsOn(sourceSet.getOutput().getClassesDirs()));
project.getPlugins().apply(JavaBasePlugin.class);
var javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class);
var sourceSets = javaPluginExtension.getSourceSets();
var tasks = project.getTasks();
project.getPlugins().withType(JavaPlugin.class, javaPlugin -> {
NamedDomainObjectProvider<SourceSet> sourceSet = sourceSets.named(SourceSet.TEST_SOURCE_SET_NAME);
setupTaskForSourceSet(project, sourceSet, t -> {
t.getSuffixes().convention(List.of("Tests"));
t.getBaseClasses().convention(List.of("org.apache.lucene.tests.util.LuceneTestCase"));
});
return testingConventions;
});
project.getPlugins().withType(InternalYamlRestTestPlugin.class, yamlRestTestPlugin -> {
NamedDomainObjectProvider<SourceSet> sourceSet = sourceSets.named(InternalYamlRestTestPlugin.SOURCE_SET_NAME);
setupTaskForSourceSet(project, sourceSet, t -> {
t.getSuffixes().convention(List.of("IT"));
t.getBaseClasses().convention(List.of("org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase"));
});
});
project.getPlugins().withType(InternalClusterTestPlugin.class, internalClusterTestPlugin -> {
NamedDomainObjectProvider<SourceSet> sourceSet = sourceSets.named(InternalClusterTestPlugin.SOURCE_SET_NAME);
setupTaskForSourceSet(project, sourceSet, t -> {
// Unfortunately we see both in our build, so we by default support both for now.
t.getSuffixes().convention(List.of("IT", "Tests"));
t.getBaseClasses()
.convention(
List.of(
"org.elasticsearch.test.ESIntegTestCase",
"org.elasticsearch.test.ESSingleNodeTestCase",
"org.elasticsearch.test.rest.ESRestTestCase",
"org.elasticsearch.test.AbstractMultiClustersTestCase"
)
);
});
});
project.getPlugins().withType(InternalJavaRestTestPlugin.class, javaRestTestPlugin -> {
NamedDomainObjectProvider<SourceSet> sourceSet = sourceSets.named(InternalJavaRestTestPlugin.SOURCE_SET_NAME);
setupTaskForSourceSet(project, sourceSet, t -> {
t.getSuffixes().convention(List.of("IT"));
t.getBaseClasses()
.convention(List.of("org.elasticsearch.test.ESIntegTestCase", "org.elasticsearch.test.rest.ESRestTestCase"));
});
});
// Create a convenience task for all checks (this does not conflict with extension, as it has higher priority in DSL):
return tasks.register(TESTING_CONVENTIONS_TASK_NAME, task -> {
task.setDescription("Runs all testing conventions checks.");
task.dependsOn(tasks.withType(TestingConventionsCheckTask.class));
});
}
private void setupTaskForSourceSet(
Project project,
NamedDomainObjectProvider<SourceSet> sourceSetProvider,
Action<TestingConventionsCheckTask> config
) {
sourceSetProvider.configure(sourceSet -> {
String taskName = sourceSet.getTaskName(null, TESTING_CONVENTIONS_TASK_NAME);
TaskProvider<TestingConventionsCheckTask> register = project.getTasks()
.register(taskName, TestingConventionsCheckTask.class, task -> {
task.getTestClassesDirs().from(sourceSet.getOutput().getClassesDirs());
task.getClasspath().from(sourceSet.getRuntimeClasspath());
});
register.configure(config);
});
}
}

View File

@ -1,422 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.internal.precommit;
import groovy.lang.Closure;
import org.gradle.api.DefaultTask;
import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileTree;
import org.gradle.api.file.ProjectLayout;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.SourceSetContainer;
import org.gradle.api.tasks.TaskAction;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.inject.Inject;
public class TestingConventionsTasks extends DefaultTask {
private static final String TEST_METHOD_PREFIX = "test";
private Map<String, File> testClassNames;
private NamedDomainObjectContainer<TestingConventionRule> naming;
private ProjectLayout projectLayout;
private SourceSetContainer sourceSets;
private Provider<Map<String, Set<File>>> candidateClassFilesProvider;
private Map<String, Set<File>> candidateClassFiles;
public void setCandidateClassFilesProvider(Provider<Map<String, Set<File>>> candidateClassFilesProvider) {
this.candidateClassFilesProvider = candidateClassFilesProvider;
}
@Inject
public TestingConventionsTasks(ProjectLayout projectLayout, ObjectFactory objectFactory) {
this.projectLayout = projectLayout;
this.naming = objectFactory.domainObjectContainer(TestingConventionRule.class);
setDescription("Tests various testing conventions");
}
@Input
public Map<String, Set<File>> getClassFilesPerEnabledTask() {
candidateClassFiles = candidateClassFilesProvider.get();
return candidateClassFiles;
}
@Input
public Map<String, File> getTestClassNames() {
if (testClassNames == null) {
testClassNames = sourceSets.getByName(SourceSet.TEST_SOURCE_SET_NAME)
.getOutput()
.getClassesDirs()
.getFiles()
.stream()
.filter(File::exists)
.flatMap(testRoot -> walkPathAndLoadClasses(testRoot).entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
return testClassNames;
}
@Input
public NamedDomainObjectContainer<TestingConventionRule> getNaming() {
return naming;
}
@OutputFile
public File getSuccessMarker() {
return new File(projectLayout.getBuildDirectory().getAsFile().get(), "markers/" + getName());
}
public void naming(Closure<?> action) {
naming.configure(action);
}
@Input
public Set<String> getMainClassNamedLikeTests() {
if (sourceSets.findByName(SourceSet.MAIN_SOURCE_SET_NAME) == null) {
// some test projects don't have a main source set
return Collections.emptySet();
}
return sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME)
.getOutput()
.getClassesDirs()
.getAsFileTree()
.getFiles()
.stream()
.filter(file -> file.getName().endsWith(".class"))
.map(File::getName)
.map(name -> name.substring(0, name.length() - 6))
.filter(this::implementsNamingConvention)
.collect(Collectors.toSet());
}
@TaskAction
public void doCheck() throws IOException {
final String problems;
try (
URLClassLoader isolatedClassLoader = new URLClassLoader(
getTestsClassPath().getFiles().stream().map(this::fileToUrl).toArray(URL[]::new)
)
) {
Predicate<Class<?>> isStaticClass = clazz -> Modifier.isStatic(clazz.getModifiers());
Predicate<Class<?>> isPublicClass = clazz -> Modifier.isPublic(clazz.getModifiers());
Predicate<Class<?>> isAbstractClass = clazz -> Modifier.isAbstract(clazz.getModifiers());
final Map<File, ? extends Class<?>> classes = getTestClassNames().entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getValue, entry -> loadClassWithoutInitializing(entry.getKey(), isolatedClassLoader)));
final FileTree allTestClassFiles = projectLayout.files(
classes.values()
.stream()
.filter(isStaticClass.negate())
.filter(isPublicClass)
.filter((Predicate<Class<?>>) this::implementsNamingConvention)
.map(clazz -> testClassNames.get(clazz.getName()))
.collect(Collectors.toList())
).getAsFileTree();
final Map<String, Set<File>> classFilesPerTask = candidateClassFiles;
final Set<File> testSourceSetFiles = sourceSets.getByName(SourceSet.TEST_SOURCE_SET_NAME).getRuntimeClasspath().getFiles();
final Map<String, Set<Class<?>>> testClassesPerTask = classFilesPerTask.entrySet()
.stream()
.filter(entry -> testSourceSetFiles.containsAll(entry.getValue()))
.collect(
Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue()
.stream()
.map(classes::get)
.filter(this::implementsNamingConvention)
.collect(Collectors.toSet())
)
);
final Map<String, Set<Class<?>>> suffixToBaseClass;
if (classes.isEmpty()) {
// Don't load base classes if we don't have any tests.
// This allows defaults to be configured for projects that don't have any tests
//
suffixToBaseClass = Collections.emptyMap();
} else {
suffixToBaseClass = naming.stream()
.collect(
Collectors.toMap(
TestingConventionRule::getSuffix,
rule -> rule.getBaseClasses()
.stream()
.map(each -> loadClassWithoutInitializing(each, isolatedClassLoader))
.collect(Collectors.toSet())
)
);
}
problems = collectProblems(
checkNoneExists(
"Test classes implemented by inner classes will not run",
classes.values()
.stream()
.filter(isStaticClass)
.filter(isPublicClass)
.filter(((Predicate<Class<?>>) this::implementsNamingConvention).or(this::seemsLikeATest))
),
checkNoneExists(
"Seem like test classes but don't match naming convention",
classes.values()
.stream()
.filter(isStaticClass.negate())
.filter(isPublicClass)
.filter(isAbstractClass.negate())
.filter(this::seemsLikeATest) // TODO when base classes are set, check for classes that extend them
.filter(((Predicate<Class<?>>) this::implementsNamingConvention).negate())
),
// TODO: check for non public classes that seem like tests
// TODO: check for abstract classes that implement the naming conventions
// No empty enabled tasks
collectProblems(
testClassesPerTask.entrySet()
.stream()
.map(entry -> checkAtLeastOneExists("test class included in task " + entry.getKey(), entry.getValue().stream()))
.sorted()
.collect(Collectors.joining("\n"))
),
checkNoneExists(
"Test classes are not included in any enabled task ("
+ classFilesPerTask.keySet().stream().collect(Collectors.joining(","))
+ ")",
allTestClassFiles.getFiles()
.stream()
.filter(testFile -> classFilesPerTask.values().stream().anyMatch(fileSet -> fileSet.contains(testFile)) == false)
.map(classes::get)
),
collectProblems(suffixToBaseClass.entrySet().stream().filter(entry -> entry.getValue().isEmpty() == false).map(entry -> {
return checkNoneExists(
"Tests classes with suffix `"
+ entry.getKey()
+ "` should extend "
+ entry.getValue().stream().map(Class::getName).collect(Collectors.joining(" or "))
+ " but the following classes do not",
classes.values()
.stream()
.filter(clazz -> clazz.getName().endsWith(entry.getKey()))
.filter(clazz -> entry.getValue().stream().anyMatch(test -> test.isAssignableFrom(clazz)) == false)
);
}).sorted().collect(Collectors.joining("\n"))),
// TODO: check that the testing tasks are included in the right task based on the name ( from the rule )
checkNoneExists("Classes matching the test naming convention should be in test not main", getMainClassNamedLikeTests())
);
}
if (problems.isEmpty()) {
getSuccessMarker().getParentFile().mkdirs();
Files.write(getSuccessMarker().toPath(), new byte[] {}, StandardOpenOption.CREATE);
} else {
getLogger().error(problems);
throw new IllegalStateException(String.format("Testing conventions [%s] are not honored", problems));
}
}
private String collectProblems(String... problems) {
return Stream.of(problems).map(String::trim).filter(s -> s.isEmpty() == false).collect(Collectors.joining("\n"));
}
private String checkNoneExists(String message, Stream<? extends Class<?>> stream) {
String problem = stream.map(each -> " * " + each.getName()).sorted().collect(Collectors.joining("\n"));
if (problem.isEmpty() == false) {
return message + ":\n" + problem;
} else {
return "";
}
}
private String checkNoneExists(String message, Set<? extends String> candidates) {
String problem = candidates.stream().map(each -> " * " + each).sorted().collect(Collectors.joining("\n"));
if (problem.isEmpty() == false) {
return message + ":\n" + problem;
} else {
return "";
}
}
private String checkAtLeastOneExists(String message, Stream<? extends Class<?>> stream) {
if (stream.findAny().isPresent()) {
return "";
} else {
return "Expected at least one " + message + ", but found none.";
}
}
private boolean seemsLikeATest(Class<?> clazz) {
try {
ClassLoader classLoader = clazz.getClassLoader();
Class<?> junitTest = loadClassWithoutInitializing("org.junit.Assert", classLoader);
if (junitTest.isAssignableFrom(clazz)) {
getLogger().debug("{} is a test because it extends {}", clazz.getName(), junitTest.getName());
return true;
}
Class<?> junitAnnotation = loadClassWithoutInitializing("org.junit.Test", classLoader);
for (Method method : clazz.getMethods()) {
if (matchesTestMethodNamingConvention(method)) {
getLogger().debug("{} is a test because it has method named '{}'", clazz.getName(), method.getName());
return true;
}
if (isAnnotated(method, junitAnnotation)) {
getLogger().debug(
"{} is a test because it has method '{}' annotated with '{}'",
clazz.getName(),
method.getName(),
junitAnnotation.getName()
);
return true;
}
}
return false;
} catch (NoClassDefFoundError e) {
// Include the message to get more info to get more a more useful message when running Gradle without -s
throw new IllegalStateException("Failed to inspect class " + clazz.getName() + ". Missing class? " + e.getMessage(), e);
}
}
private boolean implementsNamingConvention(Class<?> clazz) {
Objects.requireNonNull(clazz);
return implementsNamingConvention(clazz.getName());
}
private boolean implementsNamingConvention(String className) {
if (naming.stream().map(TestingConventionRule::getSuffix).anyMatch(suffix -> className.endsWith(suffix))) {
getLogger().debug("{} is a test because it matches the naming convention", className);
return true;
}
return false;
}
private boolean matchesTestMethodNamingConvention(Method method) {
return method.getName().startsWith(TEST_METHOD_PREFIX) && Modifier.isStatic(method.getModifiers()) == false;
}
private boolean isAnnotated(Method method, Class<?> annotation) {
for (Annotation presentAnnotation : method.getAnnotations()) {
if (annotation.isAssignableFrom(presentAnnotation.getClass())) {
return true;
}
}
return false;
}
@Classpath
public FileCollection getTestsClassPath() {
return sourceSets.getByName(SourceSet.TEST_SOURCE_SET_NAME).getRuntimeClasspath();
}
private Map<String, File> walkPathAndLoadClasses(File testRoot) {
Map<String, File> classes = new HashMap<>();
try {
Files.walkFileTree(testRoot.toPath(), new FileVisitor<Path>() {
private String packageName;
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
// First we visit the root directory
if (packageName == null) {
// And it package is empty string regardless of the directory name
packageName = "";
} else {
packageName += dir.getFileName() + ".";
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
// Go up one package by jumping back to the second to last '.'
packageName = packageName.substring(0, 1 + packageName.lastIndexOf('.', packageName.length() - 2));
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String filename = file.getFileName().toString();
if (filename.endsWith(".class")) {
String className = filename.substring(0, filename.length() - ".class".length());
classes.put(packageName + className, file.toFile());
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
throw new IOException("Failed to visit " + file, exc);
}
});
} catch (IOException e) {
throw new IllegalStateException(e);
}
return classes;
}
private Class<?> loadClassWithoutInitializing(String name, ClassLoader isolatedClassLoader) {
try {
return Class.forName(
name,
// Don't initialize the class to save time. Not needed for this test and this doesn't share a VM with any other tests.
false,
isolatedClassLoader
);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Failed to load class " + name + ". Incorrect test runtime classpath?", e);
}
}
private URL fileToUrl(File file) {
try {
return file.toURI().toURL();
} catch (MalformedURLException e) {
throw new IllegalStateException(e);
}
}
public void setSourceSets(SourceSetContainer sourceSets) {
this.sourceSets = sourceSets;
}
}

View File

@ -89,13 +89,21 @@ public class RestResourcesPlugin implements Plugin<Project> {
Configuration testConfig = project.getConfigurations().create("restTestConfig");
Configuration xpackTestConfig = project.getConfigurations().create("restXpackTestConfig");
// core
Dependency restTestdependency = project.getDependencies().project(Map.of("path", ":rest-api-spec", "configuration", "restTests"));
project.getDependencies().add(testConfig.getName(), restTestdependency);
// x-pack
Dependency restXPackTestdependency = project.getDependencies()
.project(Map.of("path", ":x-pack:plugin", "configuration", "restXpackTests"));
project.getDependencies().add(xpackTestConfig.getName(), restXPackTestdependency);
// we guard this reference to :rest-api-spec with a find to make testing easier
var restApiSpecProjectAvailable = project.findProject(":rest-api-spec") != null;
if (restApiSpecProjectAvailable) {
Dependency restTestdependency = project.getDependencies()
.project(Map.of("path", ":rest-api-spec", "configuration", "restTests"));
project.getDependencies().add(testConfig.getName(), restTestdependency);
}
// x-pack
var restXpackTests = project.findProject(":x-pack:plugin") != null;
if (restXpackTests) {
Dependency restXPackTestdependency = project.getDependencies()
.project(Map.of("path", ":x-pack:plugin", "configuration", "restXpackTests"));
project.getDependencies().add(xpackTestConfig.getName(), restXPackTestdependency);
}
project.getConfigurations().create("restTests");
project.getConfigurations().create("restXpackTests");
@ -114,8 +122,11 @@ public class RestResourcesPlugin implements Plugin<Project> {
// api
Configuration specConfig = project.getConfigurations().create("restSpec"); // name chosen for passivity
Dependency restSpecDependency = project.getDependencies().project(Map.of("path", ":rest-api-spec", "configuration", "restSpecs"));
project.getDependencies().add(specConfig.getName(), restSpecDependency);
if (restApiSpecProjectAvailable) {
Dependency restSpecDependency = project.getDependencies()
.project(Map.of("path", ":rest-api-spec", "configuration", "restSpecs"));
project.getDependencies().add(specConfig.getName(), restSpecDependency);
}
project.getConfigurations().create("restSpecs");
Provider<CopyRestApiTask> copyRestYamlApiTask = project.getTasks()

View File

@ -49,7 +49,11 @@ public class RestTestUtil {
* Setup the dependencies needed for the YAML REST tests.
*/
public static void setupYamlRestTestDependenciesDefaults(Project project, SourceSet sourceSet) {
project.getDependencies().add(sourceSet.getImplementationConfigurationName(), project.project(":test:yaml-rest-runner"));
Project yamlTestRunnerProject = project.findProject(":test:yaml-rest-runner");
// we shield the project dependency to make integration tests easier
if (yamlTestRunnerProject != null) {
project.getDependencies().add(sourceSet.getImplementationConfigurationName(), yamlTestRunnerProject);
}
}
/**
@ -57,6 +61,10 @@ public class RestTestUtil {
*/
public static void setupJavaRestTestDependenciesDefaults(Project project, SourceSet sourceSet) {
// TODO: this should just be test framework, but some cleanup is needed in places incorrectly specifying java vs yaml
project.getDependencies().add(sourceSet.getImplementationConfigurationName(), project.project(":test:yaml-rest-runner"));
// we shield the project dependency to make integration tests easier
Project yamlTestRunnerProject = project.findProject(":test:yaml-rest-runner");
if (yamlTestRunnerProject != null) {
project.getDependencies().add(sourceSet.getImplementationConfigurationName(), yamlTestRunnerProject);
}
}
}

View File

@ -17,7 +17,6 @@ import com.avast.gradle.dockercompose.tasks.ComposeUp;
import org.elasticsearch.gradle.internal.docker.DockerSupportPlugin;
import org.elasticsearch.gradle.internal.docker.DockerSupportService;
import org.elasticsearch.gradle.internal.info.BuildParams;
import org.elasticsearch.gradle.internal.precommit.TestingConventionsTasks;
import org.elasticsearch.gradle.test.SystemPropertyCommandLineArgumentProvider;
import org.elasticsearch.gradle.util.GradleUtils;
import org.gradle.api.Action;
@ -139,7 +138,6 @@ public class TestFixturesPlugin implements Plugin<Project> {
// Skip docker compose tasks if it is unavailable
maybeSkipTasks(tasks, dockerSupport, Test.class);
maybeSkipTasks(tasks, dockerSupport, getTaskClass("org.elasticsearch.gradle.internal.test.RestIntegTestTask"));
maybeSkipTasks(tasks, dockerSupport, TestingConventionsTasks.class);
maybeSkipTasks(tasks, dockerSupport, getTaskClass("org.elasticsearch.gradle.internal.test.AntFixture"));
maybeSkipTasks(tasks, dockerSupport, ComposeUp.class);
maybeSkipTasks(tasks, dockerSupport, ComposePull.class);

View File

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public class NamingConventionIT {
}

View File

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public class NamingConventionTests {
}

View File

@ -1,80 +0,0 @@
plugins {
id 'elasticsearch.build' apply false
}
allprojects {
apply plugin: 'java'
apply plugin: org.elasticsearch.gradle.internal.precommit.TestingConventionsPrecommitPlugin
repositories {
mavenCentral()
}
dependencies {
testImplementation "junit:junit:4.12"
}
testingConventions.naming {
// Reset default to no baseClass checks
Tests {
baseClasses = []
}
IT {
baseClasses = []
}
}
}
project(':empty_test_task') {
tasks.register("emptyTest", Test)
}
project(':all_classes_in_tasks') {
test {
include "**/Convention*"
}
}
project(':not_implementing_base') {
testingConventions.naming {
Tests {
baseClass 'org.elasticsearch.gradle.testkit.Unit'
}
IT {
baseClass 'org.elasticsearch.gradle.testkit.Integration'
}
}
test {
include "**/*IT.class"
include "**/*Tests.class"
}
}
project(':valid_setup_no_base') {
test {
include "**/*IT.class"
include "**/*Tests.class"
}
}
project(':tests_in_main') {
}
project(':valid_setup_with_base') {
test {
include "**/*IT.class"
include "**/*Tests.class"
}
testingConventions.naming {
Tests {
baseClass 'org.elasticsearch.gradle.testkit.Unit'
}
IT {
baseClass 'org.elasticsearch.gradle.testkit.Integration'
}
}
}

View File

@ -1,19 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
import org.junit.Test;
public class LooksLikeATestWithoutNamingConvention1 {
@Test
public void annotatedTestMethod() {
}
}

View File

@ -1,14 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
import org.junit.Assert;
public class LooksLikeATestWithoutNamingConvention2 extends Assert {
}

View File

@ -1,16 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public class LooksLikeATestWithoutNamingConvention3 {
public void testMethod() {
}
}

View File

@ -1,16 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public abstract class LooksLikeTestsButAbstract {
public void testMethod() {
}
}

View File

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public class NamingConventionIT {
}

View File

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public class NamingConventionTests {
}

View File

@ -1,53 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
import org.junit.Assert;
import org.junit.Test;
public class NastyInnerClasses {
public static class NamingConventionTests {
}
public static class NamingConventionIT {
}
public static class LooksLikeATestWithoutNamingConvention1 {
@Test
public void annotatedTestMethod() {
}
}
public static class LooksLikeATestWithoutNamingConvention2 extends Assert {
}
public static class LooksLikeATestWithoutNamingConvention3 {
public void testMethod() {
}
}
static abstract public class NonOffendingAbstractTests {
}
private static class NonOffendingPrivateTests {
}
static class NonOffendingPackageTests {
}
}

View File

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public abstract class AbstractIT {
}

View File

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public class Integration {
}

View File

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public class NamingConventionIT {
}

View File

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public class NamingConventionMissmatchIT extends Unit {
}

View File

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public class NamingConventionMissmatchTests extends Integration {
}

View File

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public class NamingConventionTests {
}

View File

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public class Unit {
}

View File

@ -1,8 +0,0 @@
include 'no_tests_in_inner_classes'
include 'incorrect_naming_conventions'
include 'empty_test_task'
include 'all_classes_in_tasks'
include 'not_implementing_base'
include 'valid_setup_no_base'
include 'valid_setup_with_base'
include 'tests_in_main'

View File

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public class NamingConventionIT {
}

View File

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public class NamingConventionTests {
}

View File

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public class NamingConventionIT {
}

View File

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public class NamingConventionTests {
}

View File

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public class Integration {
}

View File

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public class NamingConventionIT extends Integration {
}

View File

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public class NamingConventionTests extends Unit {
}

View File

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.gradle.testkit;
public class Unit {
}

View File

@ -10,14 +10,44 @@ package org.elasticsearch.gradle.fixtures
class AbstractJavaGradleFuncTest extends AbstractGradleFuncTest {
File javaMainClass() {
file('src/main/java/org/acme/JavaMainClass.java') << """
public class JavaMainClass {
}
"""
File testClazz(String testClassName) {
testClazz(testClassName, null)
}
File testClazz(String testClassName, String parent) {
testClazz(testClassName, parent, null)
}
File testClazz(String testClassName, Closure<String> content) {
testClazz(testClassName, null, content)
}
File testClazz(String testClassName, String parent, Closure<String> content) {
clazz(dir("src/test/java"), testClassName, parent, content)
}
File clazz(File sourceDir, String className, String parent = null, Closure<String> content = null) {
def classFile = new File(sourceDir, "${className.replace('.', '/')}.java")
classFile.getParentFile().mkdirs()
writeClazz(className, parent, classFile, content)
}
File clazz(String className, parent = null, Closure<String> content = null) {
def classFile = file("src/main/java/${className.replace('.', '/')}.java")
writeClazz(className, parent, classFile, content)
}
static File writeClazz(String className, String parent, File classFile, Closure<String> content) {
def packageName = className.substring(0, className.lastIndexOf('.'))
def simpleClassName = className.substring(className.lastIndexOf('.') + 1)
classFile << """
package ${packageName};
public class ${simpleClassName} ${parent == null ? "" : "extends $parent"} {
${content == null ? "" : content.call()}
}
"""
classFile
}
}

View File

@ -76,13 +76,8 @@ tasks.named("jarHell").configure {
enabled = false
}
tasks.named("testingConventions").configure {
naming.clear()
naming {
Tests {
baseClass 'org.elasticsearch.client.RestClientTestCase'
}
}
tasks.named('testTestingConventions').configure {
baseClass 'org.elasticsearch.client.RestClientTestCase'
}
tasks.named("thirdPartyAudit").configure {

View File

@ -50,6 +50,7 @@ tasks.named('forbiddenApisMain').configure {
replaceSignatureFiles 'jdk-signatures'
}
tasks.named('forbiddenApisTest').configure {
//we are using jdk-internal instead of jdk-non-portable to allow for com.sun.net.httpserver.* usage
bundledSignatures -= 'jdk-non-portable'
@ -67,13 +68,8 @@ tasks.named("dependencyLicenses").configure {
// TODO: Not anymore. Now in :libs:elasticsearch-core
tasks.named("jarHell").configure { enabled = false }
tasks.named("testingConventions").configure {
naming.clear()
naming {
Tests {
baseClass 'org.elasticsearch.client.RestClientTestCase'
}
}
tasks.named("testTestingConventions").configure {
baseClass 'org.elasticsearch.client.RestClientTestCase'
}
tasks.named("thirdPartyAudit").configure {

View File

@ -25,12 +25,6 @@ tasks.named('forbiddenApisMain').configure {
// JAR hell is part of core which we do not want to add as a dependency
tasks.named("jarHell").configure { enabled = false }
tasks.named("testingConventions").configure {
naming.clear()
naming {
Tests {
baseClass 'junit.framework.TestCase'
}
}
tasks.named("testTestingConventions").configure {
baseClass 'junit.framework.TestCase'
}

View File

@ -21,6 +21,11 @@ esplugin {
}
}
tasks.named('internalClusterTestTestingConventions').configure {
baseClass 'org.elasticsearch.ingest.geoip.AbstractGeoIpIT'
baseClass 'org.elasticsearch.test.ESTestCase'
}
dependencies {
implementation('com.maxmind.geoip2:geoip2:3.0.0')
// geoip2 dependencies:

View File

@ -115,5 +115,4 @@ if (BuildParams.inFipsJvm) {
tasks.named("test").configure { enabled = false }
tasks.named("yamlRestTest").configure { enabled = false };
tasks.named("yamlRestTestV7CompatTest").configure { enabled = false };
tasks.named("testingConventions").configure { enabled = false };
}

View File

@ -22,6 +22,10 @@ if (BuildParams.isSnapshotBuild() == false) {
}
}
tasks.named('internalClusterTestTestingConventions').configure {
baseClass 'org.elasticsearch.index.mapper.MapperTestCase'
}
restResources {
restApi {
include '_common', 'indices', 'index', 'search'

View File

@ -17,14 +17,11 @@ testClusters.configureEach {
extraConfigFile 'ingest-geoip/GeoLite2-City.mmdb', file("src/yamlRestTest/resources/GeoLite2-City.mmdb")
}
//tasks.named("testingConventions").configure {
// naming {
// IT {
// baseClass 'org.elasticsearch.ingest.AbstractScriptTestCase'
// }
// }
//}
tasks.named("yamlRestTestTestingConventions").configure {
baseClass 'org.elasticsearch.ingest.AbstractScriptTestCase'
baseClass 'org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase'
}
tasks.named("forbiddenPatterns").configure {
exclude '**/*.mmdb'
}
}

View File

@ -90,17 +90,10 @@ tasks.named('forbiddenApisTest').configure {
addSignatureFiles 'hppc-signatures'
}
tasks.named("testingConventions").configure {
naming.clear()
naming {
Tests {
baseClass "org.apache.lucene.tests.util.LuceneTestCase"
}
IT {
baseClass "org.elasticsearch.test.ESIntegTestCase"
baseClass "org.elasticsearch.test.ESSingleNodeTestCase"
}
}
tasks.named('internalClusterTestTestingConventions').configure {
baseClass "org.elasticsearch.test.AbstractMultiClustersTestCase"
baseClass "org.elasticsearch.test.ESIntegTestCase"
baseClass "org.elasticsearch.test.ESSingleNodeTestCase"
}
File generatedResourcesDir = new File(buildDir, 'generated-resources')

View File

@ -13,7 +13,7 @@ import org.elasticsearch.common.unit.ByteSizeUnit;
import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.repositories.blobstore.ESFsBasedRepositoryIntegTestCase;
public class FsBlobStoreRepositoryIntegTests extends ESFsBasedRepositoryIntegTestCase {
public class FsBlobStoreRepositoryIT extends ESFsBasedRepositoryIntegTestCase {
@Override
protected Settings repositorySettings(String repositoryName) {

View File

@ -32,6 +32,13 @@ dependencies {
api "org.elasticsearch:mocksocket:${versions.mocksocket}"
}
sourceSets {
integTest {
compileClasspath += sourceSets["main"].output + configurations["testRuntimeClasspath"]
runtimeClasspath += output + compileClasspath
}
}
// the main files are actually test files, so use the appropriate forbidden api sigs
tasks.named('forbiddenApisMain').configure {
replaceSignatureFiles 'jdk-signatures', 'es-all-signatures', 'es-test-signatures'
@ -41,7 +48,6 @@ tasks.named('forbiddenApisMain').configure {
tasks.named("dependencyLicenses").configure { enabled = false }
tasks.named("dependenciesInfo").configure { enabled = false }
tasks.named("dependenciesGraph").configure { enabled = false }
tasks.named("thirdPartyAudit").configure {
ignoreMissingClasses(
// classes are missing
@ -74,7 +80,12 @@ tasks.named("test").configure {
}
tasks.register("integTest", Test) {
include "**/*IT.class"
testClassesDirs = sourceSets.integTest.output.classesDirs
classpath = sourceSets.integTest.runtimeClasspath
}
tasks.named('check').configure {
dependsOn 'integTest'
}
tasks.register("verifyVersions") {

View File

@ -22,7 +22,7 @@ import org.elasticsearch.xpack.autoscaling.action.GetAutoscalingCapacityAction;
import java.util.ArrayList;
import java.util.Collection;
public class AutoscalingStorageIntegTestCase extends DiskUsageIntegTestCase {
public abstract class AutoscalingStorageIntegTestCase extends DiskUsageIntegTestCase {
protected static final long HIGH_WATERMARK_BYTES = 10240;
protected static final long LOW_WATERMARK_BYTES = 2 * HIGH_WATERMARK_BYTES;

View File

@ -25,6 +25,12 @@ tasks.named('internalClusterTest').configure {
exclude noSecurityManagerITClasses
}
tasks.named('internalClusterTestTestingConventions').configure {
baseClass 'org.elasticsearch.xpack.CcrIntegTestCase'
baseClass 'org.elasticsearch.xpack.CcrSingleNodeTestCase'
baseClass 'org.elasticsearch.test.ESIntegTestCase'
}
addQaCheckDependencies(project)
dependencies {
@ -36,11 +42,3 @@ dependencies {
testImplementation(project(":modules:analysis-common"))
testImplementation(project(":modules:data-streams"))
}
tasks.named("testingConventions").configure {
naming {
IT {
baseClass "org.elasticsearch.xpack.CcrIntegTestCase"
}
}
}

View File

@ -294,6 +294,4 @@ addQaCheckDependencies(project)
if (BuildParams.inFipsJvm) {
// We don't support the IDP in FIPS-140 mode, so no need to run tests
tasks.named("test").configure { enabled = false }
// We run neither integTests nor unit tests in FIPS-140 mode
tasks.named("testingConventions").configure { enabled = false }
}

View File

@ -59,7 +59,4 @@ tasks.named("check").configure { dependsOn 'follow-cluster' }
// Security is explicitly disabled for follow-cluster and leader-cluster, do not run these in FIPS mode
tasks.withType(Test).configureEach {
enabled = BuildParams.inFipsJvm == false
}
tasks.named("testingConventions").configure {
enabled = BuildParams.inFipsJvm == false
}
}

View File

@ -18,6 +18,10 @@ dependencies {
internalClusterTestImplementation(testArtifact(project(xpackModule('core'))))
}
tasks.named('internalClusterTestTestingConventions').configure {
baseClass 'org.elasticsearch.index.mapper.MapperTestCase'
}
if (BuildParams.isSnapshotBuild() == false) {
tasks.named("internalClusterTest").configure {
systemProperty 'es.index_mode_feature_flag_registered', 'true'

View File

@ -12,3 +12,8 @@ if (BuildParams.inFipsJvm){
// Test clusters run with security disabled
tasks.named("javaRestTest").configure{enabled = false }
}
tasks.named('javaRestTestTestingConventions').configure {
baseClass "org.elasticsearch.xpack.ml.integration.InferenceTestCase"
baseClass "org.elasticsearch.test.rest.ESRestTestCase"
}

View File

@ -22,7 +22,7 @@ import java.util.Set;
import static org.hamcrest.Matchers.equalTo;
public class InferenceTestCase extends ESRestTestCase {
public abstract class InferenceTestCase extends ESRestTestCase {
protected final Set<String> createdPipelines = new HashSet<>();

View File

@ -14,7 +14,7 @@ import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.common.unit.RatioValue;
import org.elasticsearch.xpack.searchablesnapshots.cache.shared.FrozenCacheService;
public class BaseFrozenSearchableSnapshotsIntegTestCase extends BaseSearchableSnapshotsIntegTestCase {
public abstract class BaseFrozenSearchableSnapshotsIntegTestCase extends BaseSearchableSnapshotsIntegTestCase {
@Override
protected boolean forceSingleDataPath() {
return true;

View File

@ -49,9 +49,6 @@ if (BuildParams.inFipsJvm) {
tasks.named("jarHell").configure {
enabled = false
}
tasks.named("testingConventions").configure {
enabled = false
}
// Forbiden APIs non-portable checks fail because bouncy castle classes being used from the FIPS JDK since those are
// not part of the Java specification - all of this is as designed, so we have to relax this check for FIPS.
tasks.withType(CheckForbiddenApis).configureEach {

View File

@ -1,19 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
package org.elasticsearch.xpack.security.operator;
import org.elasticsearch.test.ESTestCase;
// This test class is really to pass the testingConventions test
public class OperatorPrivilegesTestPluginTests extends ESTestCase {
public void testPluginWillInstantiate() {
final OperatorPrivilegesTestPlugin operatorPrivilegesTestPlugin = new OperatorPrivilegesTestPlugin();
}
}

View File

@ -24,7 +24,7 @@ import static org.hamcrest.Matchers.equalTo;
// This test is to ensure that an operator can always successfully perform operator-only actions
// even when operator privileges are partially enabled for the cluster. This could happen
// for a cluster with operator privileges disabled wanting to enable operator privileges with rolling upgrade.
public class OperatorPrivilegesPartiallyEnabledIntegTestCase extends SecurityIntegTestCase {
public class OperatorPrivilegesPartiallyEnabledIntegTests extends SecurityIntegTestCase {
private static final String OPERATOR_USER_NAME = "test_operator";
private static final String OPERATOR_AUTH_HEADER = "Basic "

View File

@ -18,6 +18,11 @@ testClusters.matching { it.name == "yamlRestTest" }.configureEach {
setting 'xpack.security.enabled', 'false'
}
tasks.named('yamlRestTestTestingConventions').configure {
baseClass 'org.elasticsearch.repositories.blobstore.testkit.AbstractSnapshotRepoTestKitRestTestCase'
baseClass 'org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase'
}
restResources {
restApi {
include 'indices', 'search', 'bulk', 'snapshot', 'nodes', '_common', 'snapshot_repo_test_kit'

View File

@ -16,5 +16,3 @@ dependencies {
}
addQaCheckDependencies(project)
tasks.named("testingConventions").configure { enabled = false }

View File

@ -10,6 +10,11 @@ dependencies {
yamlRestTestImplementation(project(":client:rest-high-level"))
}
tasks.named('yamlRestTestTestingConventions').configure {
baseClass 'org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase'
baseClass 'org.elasticsearch.test.rest.ESRestTestCase'
}
tasks.named("forbiddenPatterns").configure {
exclude '**/*.key'
exclude '**/*.pem'

View File

@ -49,7 +49,6 @@ restResources {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
logger.warn("Disabling repository-old-versions tests because we can't get the pid file on windows")
tasks.named("testingConventions").configure { enabled = false }
} else {
/* Register a gradle artifact transformation to unpack resolved elasticsearch distributions. We only resolve
* zip files here. Using artifact transforms allow a better caching of the downloaded distros as the

View File

@ -79,8 +79,4 @@ BuildParams.bwcVersions.withWireCompatible { bwcVersion, baseName ->
// Security is explicitly disabled, do not run tests in FIPS mode
tasks.withType(Test).configureEach {
enabled = BuildParams.inFipsJvm == false
}
tasks.named("testingConventions").configure {
enabled = BuildParams.inFipsJvm == false
}
}

View File

@ -92,3 +92,8 @@ tasks.named("processYamlRestTestResources").configure {
filter("tokens" : expansions.collectEntries {k, v -> [k, v.toString()]} /* must be a map of strings */, ReplaceTokens.class)
}
}
tasks.named('yamlRestTestTestingConventions').configure {
baseClass 'org.elasticsearch.test.rest.ESRestTestCase'
baseClass 'org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase'
}