Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure spock 2/nebula-test 10 tests actually run #1929

Merged
merged 8 commits into from
Oct 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog/@unreleased/pr-1929.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type: fix
fix:
description: Ensure nebula-test 10/Spock 2 test run by automatically setting `useJUnitPlatform()`
on test tasks. Add extra verification to the `checkJUnitDependencies` task for
nebula-test 10/Spock 2 tests.
links:
- https://github.com/palantir/gradle-baseline/pull/1929
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,20 @@

package com.palantir.baseline.plugins;

import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableSet;
import com.palantir.baseline.tasks.CheckJUnitDependencies;
import com.palantir.baseline.tasks.CheckUnusedDependenciesTask;
import com.palantir.baseline.util.VersionUtils;
import java.util.Objects;
import java.util.Optional;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
import org.gradle.api.artifacts.result.ResolvedComponentResult;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.specs.Spec;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskContainer;
import org.gradle.api.tasks.TaskProvider;
Expand Down Expand Up @@ -61,38 +63,44 @@ public void apply(Project project) {
});

project.getPlugins().withType(JavaPlugin.class, unusedPlugin -> {
// afterEvaluate necessary because the junit-jupiter dep might be added further down the build.gradle
project.afterEvaluate(proj -> {
proj.getConvention()
.getPlugin(JavaPluginConvention.class)
.getSourceSets()
.matching(ss -> hasCompileDependenciesMatching(proj, ss, BaselineTesting::isJunitJupiter))
.forEach(ss -> {
Optional<Test> maybeTestTask = getTestTaskForSourceSet(proj, ss);
if (!maybeTestTask.isPresent()) {
log.warn("Detected 'org:junit.jupiter:junit-jupiter', but unable to find test task");
return;
}
log.info(
"Detected 'org:junit.jupiter:junit-jupiter', enabling useJUnitPlatform() on {}",
maybeTestTask.get().getName());
enableJunit5ForTestTask(maybeTestTask.get());

// Also wire up a test ignore for this source set
project.getPlugins().withType(BaselineExactDependencies.class, exactDeps -> {
TaskContainer tasks = project.getTasks();
tasks.named(
BaselineExactDependencies.checkUnusedDependenciesNameForSourceSet(ss),
CheckUnusedDependenciesTask.class,
task -> task.ignore("org.junit.jupiter", "junit-jupiter"));
});
TaskProvider<CheckJUnitDependencies> checkJUnitDependencies =
project.getTasks().register("checkJUnitDependencies", CheckJUnitDependencies.class);

project.getConvention()
.getPlugin(JavaPluginConvention.class)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use the JavaPluginExtension as conventions are deprecated and to be removed with Gradle 8.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried, but Gradle 6 doesn't have the getSourceSets method we need 118cb97

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I remember encountering this before. I guess Gradle 7 is the bridge version then at with Gradle 8 we have to cut support for Gradle 6. Fine for now then!

.getSourceSets()
.configureEach(sourceSet -> {
getTestTaskForSourceSet(project, sourceSet).ifPresent(testTask -> {
testTask.dependsOn(checkJUnitDependencies);
});
});

TaskProvider<CheckJUnitDependencies> task =
project.getTasks().register("checkJUnitDependencies", CheckJUnitDependencies.class);
ifHasResolvedCompileDependenciesMatching(
project,
sourceSet,
BaselineTesting::requiresJunitPlatform,
() -> fixSourceSet(project, sourceSet));
});
});
}

project.getTasks().findByName(JavaPlugin.TEST_TASK_NAME).dependsOn(task);
private void fixSourceSet(Project project, SourceSet ss) {
Optional<Test> maybeTestTask = getTestTaskForSourceSet(project, ss);
if (!maybeTestTask.isPresent()) {
log.warn("Detected 'org:junit.jupiter:junit-jupiter', but unable to find test task");
return;
}
log.info(
"Detected 'org:junit.jupiter:junit-jupiter', enabling useJUnitPlatform() on {}",
maybeTestTask.get().getName());
enableJunit5ForTestTask(maybeTestTask.get());

// Also wire up a test ignore for this source set
project.getPlugins().withType(BaselineExactDependencies.class, exactDeps -> {
TaskContainer tasks = project.getTasks();
tasks.named(
BaselineExactDependencies.checkUnusedDependenciesNameForSourceSet(ss),
CheckUnusedDependenciesTask.class,
task -> task.ignore("org.junit.jupiter", "junit-jupiter"));
});
}

Expand All @@ -112,20 +120,32 @@ public static Optional<Test> getTestTaskForSourceSet(Project proj, SourceSet ss)
return Optional.empty();
}

private static boolean hasCompileDependenciesMatching(Project project, SourceSet sourceSet, Spec<Dependency> spec) {
return project
.getConfigurations()
.getByName(sourceSet.getCompileClasspathConfigurationName())
.getAllDependencies()
.matching(spec)
.stream()
.findAny()
.isPresent();
private static void ifHasResolvedCompileDependenciesMatching(
Project project, SourceSet sourceSet, Predicate<ModuleComponentIdentifier> spec, Runnable runnable) {
project.getConfigurations()
.getByName(sourceSet.getRuntimeClasspathConfigurationName())
.getIncoming()
.afterResolve(deps -> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we need to distinguish between Spock 1 and Spock 2, we need to actually resolve the configuration to get a version number. It's actually quite tricky to resolve here, as it fails foul of the checks we have to prevent resolving configuration at configuration time, which destroys perf.

Instead, we wait for this configuration to resolved, which will happen before the test is run, by the checkJUnitDependencies task if nothing else...

(This would be so much easier if useJUnitPlatform was a lazy property)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was initially thinking that it might be sufficient to just go through the explicitly declared deps which would not require resolution. However most of the time Spock is brought in as a transitive and so you have to do resolution :(

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially did that and checked for nebula-test too, but until resolution happens you don't know the version number, so can't decide whether or not to do the right thing

boolean anyMatch = deps.getResolutionResult().getAllComponents().stream()
.map(ResolvedComponentResult::getId)
.filter(componentId -> componentId instanceof ModuleComponentIdentifier)
.map(componentId -> (ModuleComponentIdentifier) componentId)
.anyMatch(spec);

if (anyMatch) {
runnable.run();
}
});
}

private static boolean requiresJunitPlatform(ModuleComponentIdentifier dep) {
return isDep(dep, "org.junit.jupiter", "junit-jupiter")
|| (isDep(dep, "org.spockframework", "spock-core")
&& VersionUtils.majorVersionNumber(dep.getVersion()) >= 2);
}

private static boolean isJunitJupiter(Dependency dep) {
return Objects.equals(dep.getGroup(), "org.junit.jupiter")
&& dep.getName().equals("junit-jupiter");
private static boolean isDep(ModuleComponentIdentifier dep, String group, String name) {
return group.equals(dep.getGroup()) && name.equals(dep.getModule());
}

private static void enableJunit5ForTestTask(Test task) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import com.google.common.base.Preconditions;
import com.palantir.baseline.plugins.BaselineTesting;
import com.palantir.baseline.util.VersionUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
Expand Down Expand Up @@ -84,7 +85,8 @@ private void validateSourceSet(SourceSet ss, Test task) {
.getAllComponents();
boolean junitJupiterIsPresent = hasDep(deps, CheckJUnitDependencies::isJunitJupiter);
boolean vintageEngineExists = hasDep(deps, CheckJUnitDependencies::isVintageEngine);
boolean spockDependency = hasDep(deps, CheckJUnitDependencies::isSpock);
boolean spock1Dependency = hasDep(deps, CheckJUnitDependencies::isSpock1);
boolean spock2OrGreaterDependency = hasDep(deps, CheckJUnitDependencies::isSpock2OrGreater);
String testRuntimeOnly = ss.getRuntimeOnlyConfigurationName();
boolean junitPlatformEnabled = BaselineTesting.useJUnitPlatformEnabled(task);

Expand Down Expand Up @@ -139,23 +141,30 @@ private void validateSourceSet(SourceSet ss, Test task) {
}

// spock uses JUnit4 under the hood, so the vintage engine is critical
if (spockDependency && junitPlatformEnabled) {
if (spock1Dependency && junitPlatformEnabled) {
Preconditions.checkState(
vintageEngineExists,
"Tests may be silently not running! Spock dependency detected (which uses "
"Tests may be silently not running! Spock 1.x dependency detected (which uses "
+ "a JUnit4 Runner under the hood). Please add the following:\n\n"
+ " "
+ testRuntimeOnly
+ " 'org.junit.vintage:junit-vintage-engine'\n\n");
}

if (spock2OrGreaterDependency) {
Preconditions.checkState(
junitPlatformEnabled,
"Tests may silently be not running! Spock 2.x dependency "
+ "detected (which requires useJUnitPlatform() in order to run).");
}
}

private boolean hasDep(Set<ResolvedComponentResult> deps, Predicate<ModuleVersionIdentifier> spec) {
return deps.stream().anyMatch(component -> spec.test(component.getModuleVersion()));
}

private boolean sourceSetMentionsJUnit4(SourceSet ss) {
return !ss.getAllJava()
return !ss.getAllSource()
.filter(file -> fileContainsSubstring(
file,
l -> l.contains("org.junit.Test")
Expand All @@ -165,7 +174,7 @@ private boolean sourceSetMentionsJUnit4(SourceSet ss) {
}

private boolean sourceSetMentionsJUnit5Api(SourceSet ss) {
return !ss.getAllJava()
return !ss.getAllSource()
.filter(file -> fileContainsSubstring(file, l -> l.contains("org.junit.jupiter.api.")))
.isEmpty();
}
Expand All @@ -188,6 +197,14 @@ private static boolean isVintageEngine(ModuleVersionIdentifier dep) {
return "org.junit.vintage".equals(dep.getGroup()) && "junit-vintage-engine".equals(dep.getName());
}

private static boolean isSpock1(ModuleVersionIdentifier dep) {
return isSpock(dep) && VersionUtils.majorVersionNumber(dep.getVersion()) <= 1;
}

private static boolean isSpock2OrGreater(ModuleVersionIdentifier dep) {
return isSpock(dep) && VersionUtils.majorVersionNumber(dep.getVersion()) >= 2;
}

private static boolean isSpock(ModuleVersionIdentifier dep) {
return "org.spockframework".equals(dep.getGroup()) && "spock-core".equals(dep.getName());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* (c) Copyright 2021 Palantir Technologies Inc. All rights reserved.
*
* Licensed 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 com.palantir.baseline.util;

import com.google.common.base.Splitter;
import org.gradle.api.GradleException;

public final class VersionUtils {
public static int majorVersionNumber(String version) {
return Integer.parseInt(Splitter.on('.')
.splitToStream(version)
.findFirst()
.orElseThrow(() -> new GradleException("Cannot find major version number for version " + version)));
}

private VersionUtils() {}
}
Loading