diff --git a/.circleci/config.yml b/.circleci/config.yml index 2fe588a448..48ba552c3e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -108,14 +108,14 @@ jobs: - *restore_cache_wrapper - *restore_cache_deps - run: - name: gradlew npmTest - command: export SPOTLESS_EXCLUDE_MAVEN=true && ./gradlew npmTest --build-cache + name: gradlew testNpm + command: export SPOTLESS_EXCLUDE_MAVEN=true && ./gradlew testNpm --build-cache - store_test_results: - path: testlib/build/test-results/NpmTest + path: testlib/build/test-results/testNpm - store_test_results: - path: plugin-maven/build/test-results/NpmTest + path: plugin-maven/build/test-results/testNpm - store_test_results: - path: plugin-gradle/build/test-results/NpmTest + path: plugin-gradle/build/test-results/testNpm - run: name: gradlew test command: export SPOTLESS_EXCLUDE_MAVEN=true && ./gradlew test --build-cache @@ -141,12 +141,12 @@ jobs: - store_test_results: path: plugin-gradle/build/test-results/test - run: - name: gradlew npmTest - command: gradlew npmTest --build-cache -PSPOTLESS_EXCLUDE_MAVEN=true + name: gradlew testNpm + command: gradlew testNpm --build-cache -PSPOTLESS_EXCLUDE_MAVEN=true - store_test_results: - path: testlib/build/test-results/NpmTest + path: testlib/build/test-results/testNpm - store_test_results: - path: plugin-gradle/build/test-results/NpmTest + path: plugin-gradle/build/test-results/testNpm changelog_print: << : *env_gradle steps: diff --git a/gradle/special-tests.gradle b/gradle/special-tests.gradle index 5ba7b3b2c6..c435bc7e81 100644 --- a/gradle/special-tests.gradle +++ b/gradle/special-tests.gradle @@ -1,5 +1,5 @@ apply plugin: 'org.gradle.test-retry' - +apply plugin: 'com.adarshr.test-logger' def special = [ 'Npm', 'Black', @@ -21,7 +21,7 @@ tasks.named('test') { } special.forEach { tag -> - tasks.register("${tag}Test", Test) { + tasks.register("test${tag}", Test) { useJUnitPlatform { includeTags tag } } } diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 89aab234cf..ad93e65328 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -3,6 +3,23 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`). ## [Unreleased] +### Added +* You can now predeclare formatter dependencies in the root project. + * specify one of: + * `spotless { predeclareDeps() }` to resolve all deps from the root project, which will show up in dependency reports. + * `spotless { predeclareDepsFromBuildscript() }` to resolve all deps from `buildscript { repositories {`, which will not show up in dependency reports ([see #1027](https://github.com/diffplug/spotless/issues/1027)). + * and then below that you have a block where you simply declare each formatter which you are using, e.g. + * ``` + spotless { + ... + predeclareDepsFromBuildscript() + } + spotlessPredeclare { + java { eclipse() } + kotlin { ktfmt('0.28') } + } + ``` + * By default, Spotless resolves all dependencies per-project, and the predeclaration above is unnecessary in the vast majority of cases. ## [6.0.5] - 2021-12-16 ### Fixed @@ -42,7 +59,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( * To make this daemon-restriction obsolete, please see and upvote [#987](https://github.com/diffplug/spotless/issues/987). ### Changed * **BREAKING** Previously, many projects required `buildscript { repositories { mavenCentral() }}` at the top of their root project, because Spotless resolved its dependencies using the buildscript repositories. Spotless now resolves its dependencies from the normal project repositories of each project with a `spotless {...}` block. This means that you can remove the `buildscript {}` block, but you still need a `repositories { mavenCentral() }` (or similar) in each project which is using Spotless. ([#980](https://github.com/diffplug/spotless/pull/980), [#983](https://github.com/diffplug/spotless/pull/983)) - * If you prefer the old behavior, we are open to adding that back as a new feature, see [#984 predeclared dependencies](https://github.com/diffplug/spotless/issues/984). + * If you prefer the old behavior, it is available via [`predeclareDepsFromBuildscript()` starting in `6.1.0`](../README.md#dependency-resolution-modes). * **BREAKING** `createIndepentApplyTask(String taskName)` now requires that `taskName` does not end with `Apply` * Bump minimum required Gradle from `6.1` to `6.1.1`. * Bump default formatter versions ([#989](https://github.com/diffplug/spotless/pull/989)) diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index 821ca6c053..6edb03d322 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -86,6 +86,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui - [Multiple (or custom) language-specific blocks](#multiple-or-custom-language-specific-blocks) - [Inception (languages within languages within...)](#inception-languages-within-languages-within) - [Disabling warnings and error messages](#disabling-warnings-and-error-messages) + - [Dependency resolution modes](#dependency-resolution-modes) - [How do I preview what `spotlessApply` will do?](#how-do-i-preview-what-spotlessapply-will-do) - [Example configurations (from real-world projects)](#example-configurations-from-real-world-projects) @@ -910,6 +911,26 @@ spotless { ignoreErrorForPath('path/to/file.java') // ignore errors by all steps on this specific file ``` + +## Dependency resolution modes + +By default, Spotless resolves dependencies on a per-project basis. For very large parallel builds, this can sometimes cause problems. As an alternative, Spotless can be configured to resolve all dependencies in the root project like so: + +```gradle +spotless { + ... + predeclareDeps() +} +spotlessPredeclare { + java { eclipse() } + kotlin { ktfmt('0.28') } +} +``` + +Alternatively, you can also use `predeclareDepsFromBuildscript()` to resolve the dependencies from the buildscript repositories rather than the project repositories. + +If you use this feature, you will get an error if you use a formatter in a subproject which is not declared in the `spotlessPredeclare` block. + ## How do I preview what `spotlessApply` will do? diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java index b09a4af3d0..9da66b7929 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java @@ -73,7 +73,7 @@ public FormatExtension(SpotlessExtension spotless) { } protected final Provisioner provisioner() { - return spotless.getRegisterDependenciesTask().getTaskService().get().provisionerFor(spotless.project); + return spotless.getRegisterDependenciesTask().getTaskService().get().provisionerFor(spotless); } private String formatName() { @@ -757,16 +757,6 @@ protected void setupTask(SpotlessTask task) { } else { steps = this.steps; } - // - // By calling .hashCode, we are triggering all steps to evaluate their state, - // which triggers dependency resolution. It's important to do that here, because - // otherwise it won't happen until Gradle starts checking for task up-to-date-ness. - // For a large parallel build, the task up-to-dateness might get called on a different - // thread than the thread where task configuration happens, which will trigger a - // java.util.ConcurrentModificationException - // See https://github.com/diffplug/spotless/issues/1015 for details. - steps.hashCode(); - // task.setSteps(steps); task.setLineEndingsPolicy(getLineEndings().createPolicy(getProject().getProjectDir(), () -> totalTarget)); spotless.getRegisterDependenciesTask().hookSubprojectTask(task); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleProvisioner.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleProvisioner.java index 0587d070f6..240ffcdcdf 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleProvisioner.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleProvisioner.java @@ -19,15 +19,19 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.artifacts.dsl.DependencyHandler; import org.gradle.api.attributes.Bundling; +import org.gradle.api.initialization.dsl.ScriptHandler; +import com.diffplug.common.base.Unhandled; import com.diffplug.common.collect.ImmutableList; import com.diffplug.spotless.Provisioner; @@ -35,45 +39,82 @@ class GradleProvisioner { private GradleProvisioner() {} - static Provisioner newDedupingProvisioner(Project project) { - return new DedupingProvisioner(project); + enum Policy { + INDEPENDENT, ROOT_PROJECT, ROOT_BUILDSCRIPT; + + public DedupingProvisioner dedupingProvisioner(Project project) { + switch (this) { + case ROOT_PROJECT: + return new DedupingProvisioner(forProject(project)); + case ROOT_BUILDSCRIPT: + return new DedupingProvisioner(forRootProjectBuildscript(project)); + case INDEPENDENT: + default: + throw Unhandled.enumException(this); + } + } } static class DedupingProvisioner implements Provisioner { - private final Project project; + private final Provisioner provisioner; private final Map> cache = new HashMap<>(); - DedupingProvisioner(Project project) { - this.project = project; + DedupingProvisioner(Provisioner provisioner) { + this.provisioner = provisioner; } @Override public Set provisionWithTransitives(boolean withTransitives, Collection mavenCoordinates) { Request req = new Request(withTransitives, mavenCoordinates); - Set result = cache.get(req); + Set result; + synchronized (cache) { + result = cache.get(req); + } if (result != null) { return result; } else { - result = cache.get(req); - if (result != null) { - return result; - } else { - result = forProject(project).provisionWithTransitives(req.withTransitives, req.mavenCoords); - cache.put(req, result); + synchronized (cache) { + result = cache.get(req); + if (result == null) { + result = provisioner.provisionWithTransitives(req.withTransitives, req.mavenCoords); + cache.put(req, result); + } return result; } } } + + /** A child Provisioner which retries cached elements only. */ + final Provisioner cachedOnly = (withTransitives, mavenCoordinates) -> { + Request req = new Request(withTransitives, mavenCoordinates); + Set result; + synchronized (cache) { + result = cache.get(req); + } + if (result != null) { + return result; + } + throw new GradleException("Add a step with " + req.mavenCoords + " into the `spotlessPredeclare` block in the root project."); + }; + } + + static Provisioner forProject(Project project) { + return forConfigurationContainer(project, project.getConfigurations(), project.getDependencies()); + } + + static Provisioner forRootProjectBuildscript(Project project) { + Project rootProject = project.getRootProject(); + ScriptHandler buildscript = rootProject.getBuildscript(); + return forConfigurationContainer(rootProject, buildscript.getConfigurations(), buildscript.getDependencies()); } - private static Provisioner forProject(Project project) { - Objects.requireNonNull(project); + private static Provisioner forConfigurationContainer(Project project, ConfigurationContainer configurations, DependencyHandler dependencies) { return (withTransitives, mavenCoords) -> { try { - Configuration config = project.getConfigurations().create("spotless" + Configuration config = configurations.create("spotless" + new Request(withTransitives, mavenCoords).hashCode()); mavenCoords.stream() - .map(project.getDependencies()::create) + .map(dependencies::create) .forEach(config.getDependencies()::add); config.setDescription(mavenCoords.toString()); config.setTransitive(withTransitives); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java index e2a5145b02..a3fbdf4a81 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java @@ -15,16 +15,26 @@ */ package com.diffplug.gradle.spotless; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + import javax.inject.Inject; import org.gradle.api.DefaultTask; import org.gradle.api.provider.Property; import org.gradle.api.services.BuildServiceRegistry; +import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.TaskAction; import org.gradle.build.event.BuildEventsListenerRegistry; import com.diffplug.common.base.Preconditions; +import com.diffplug.common.io.Files; +import com.diffplug.spotless.FormatterStep; /** * NOT AN END-USER TASK, DO NOT USE FOR ANYTHING! @@ -40,11 +50,10 @@ public abstract class RegisterDependenciesTask extends DefaultTask { static final String TASK_NAME = "spotlessInternalRegisterDependencies"; void hookSubprojectTask(SpotlessTask task) { - // TODO: in the future, we might use this hook to implement #984 - // spotlessSetup { - // java { googleJavaFormat('1.2') } - // ...etc - // } + // this ensures that if a user is using predeclared dependencies, + // those predeclared deps will be resolved before they are needed + // by the child tasks + // // it's also needed to make sure that jvmLocalCache gets set // in the SpotlessTaskService before any spotless tasks run task.dependsOn(this); @@ -56,11 +65,27 @@ void setup() { BuildServiceRegistry buildServices = getProject().getGradle().getSharedServices(); getTaskService().set(buildServices.registerIfAbsent("SpotlessTaskService" + compositeBuildSuffix, SpotlessTaskService.class, spec -> {})); getBuildEventsListenerRegistry().onTaskCompletion(getTaskService()); + unitOutput = new File(getProject().getBuildDir(), "tmp/spotless-register-dependencies"); + } + + List steps = new ArrayList<>(); + + @Input + public List getSteps() { + return steps; + } + + File unitOutput; + + @OutputFile + public File getUnitOutput() { + return unitOutput; } @TaskAction - public void trivialFunction() { - // nothing to do :) + public void trivialFunction() throws IOException { + Files.createParentDirs(unitOutput); + Files.write(Integer.toString(1), unitOutput, StandardCharsets.UTF_8); } @Internal diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java index 52edf06e66..4b7c84343f 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java @@ -27,26 +27,33 @@ import org.gradle.api.Action; import org.gradle.api.GradleException; import org.gradle.api.Project; +import org.gradle.api.tasks.TaskContainer; +import org.gradle.api.tasks.TaskProvider; import com.diffplug.spotless.LineEnding; public abstract class SpotlessExtension { final Project project; + private final RegisterDependenciesTask registerDependenciesTask; protected static final String TASK_GROUP = "Verification"; protected static final String CHECK_DESCRIPTION = "Checks that sourcecode satisfies formatting steps."; protected static final String APPLY_DESCRIPTION = "Applies code formatting steps to sourcecode in-place."; static final String EXTENSION = "spotless"; + static final String EXTENSION_PREDECLARE = "spotlessPredeclare"; static final String CHECK = "Check"; static final String APPLY = "Apply"; static final String DIAGNOSE = "Diagnose"; protected SpotlessExtension(Project project) { this.project = requireNonNull(project); + this.registerDependenciesTask = findRegisterDepsTask().get(); } - abstract RegisterDependenciesTask getRegisterDependenciesTask(); + RegisterDependenciesTask getRegisterDependenciesTask() { + return registerDependenciesTask; + } /** Line endings (if any). */ LineEnding lineEndings = LineEnding.GIT_ATTRIBUTES; @@ -231,4 +238,43 @@ T instantiateFormatExtension(Class clazz) { } protected abstract void createFormatTasks(String name, FormatExtension formatExtension); + + TaskProvider findRegisterDepsTask() { + try { + return findRegisterDepsTask(RegisterDependenciesTask.TASK_NAME); + } catch (Exception e) { + // in a composite build there can be multiple Spotless plugins on the classpath, and they will each try to register + // a task on the root project with the same name. That will generate casting errors, which we can catch and try again + // with an identity-specific identifier. + // https://github.com/diffplug/spotless/pull/1001 for details + return findRegisterDepsTask(RegisterDependenciesTask.TASK_NAME + System.identityHashCode(RegisterDependenciesTask.class)); + } + } + + private TaskProvider findRegisterDepsTask(String taskName) { + TaskContainer rootProjectTasks = project.getRootProject().getTasks(); + if (!rootProjectTasks.getNames().contains(taskName)) { + return rootProjectTasks.register(taskName, RegisterDependenciesTask.class, RegisterDependenciesTask::setup); + } else { + return rootProjectTasks.named(taskName, RegisterDependenciesTask.class); + } + } + + public void predeclareDepsFromBuildscript() { + if (project.getRootProject() != project) { + throw new GradleException("predeclareDepsFromBuildscript can only be called from the root project"); + } + predeclare(GradleProvisioner.Policy.ROOT_BUILDSCRIPT); + } + + public void predeclareDeps() { + if (project.getRootProject() != project) { + throw new GradleException("predeclareDeps can only be called from the root project"); + } + predeclare(GradleProvisioner.Policy.ROOT_PROJECT); + } + + protected void predeclare(GradleProvisioner.Policy policy) { + project.getExtensions().create(SpotlessExtension.class, EXTENSION_PREDECLARE, SpotlessExtensionPredeclare.class, project, policy); + } } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java index 9473707d53..24fa7b54b0 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java @@ -23,7 +23,6 @@ import org.gradle.api.tasks.TaskProvider; public class SpotlessExtensionImpl extends SpotlessExtension { - private final TaskProvider registerDependenciesTask; final TaskProvider rootCheckTask, rootApplyTask, rootDiagnoseTask; public SpotlessExtensionImpl(Project project) { @@ -39,7 +38,6 @@ public SpotlessExtensionImpl(Project project) { rootDiagnoseTask = project.getTasks().register(EXTENSION + DIAGNOSE, task -> { task.setGroup(TASK_GROUP); // no description on purpose }); - registerDependenciesTask = findRegisterDepsTask(); project.afterEvaluate(unused -> { if (enforceCheck) { @@ -53,31 +51,6 @@ public SpotlessExtensionImpl(Project project) { }); } - private TaskProvider findRegisterDepsTask() { - try { - return findRegisterDepsTask(RegisterDependenciesTask.TASK_NAME); - } catch (Exception e) { - // in a composite build there can be multiple Spotless plugins on the classpath, and they will each try to register - // a task on the root project with the same name. That will generate casting errors, which we can catch and try again - // with an identity-specific identifier. - // https://github.com/diffplug/spotless/pull/1001 for details - return findRegisterDepsTask(RegisterDependenciesTask.TASK_NAME + System.identityHashCode(RegisterDependenciesTask.class)); - } - } - - private TaskProvider findRegisterDepsTask(String taskName) { - TaskContainer rootProjectTasks = project.getRootProject().getTasks(); - if (!rootProjectTasks.getNames().contains(taskName)) { - return rootProjectTasks.register(taskName, RegisterDependenciesTask.class, RegisterDependenciesTask::setup); - } else { - return rootProjectTasks.named(taskName, RegisterDependenciesTask.class); - } - } - - RegisterDependenciesTask getRegisterDependenciesTask() { - return registerDependenciesTask.get(); - } - @Override protected void createFormatTasks(String name, FormatExtension formatExtension) { boolean isIdeHook = project.hasProperty(IdeHook.PROPERTY); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionPredeclare.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionPredeclare.java new file mode 100644 index 0000000000..a086f1ca52 --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionPredeclare.java @@ -0,0 +1,49 @@ +/* + * Copyright 2021 DiffPlug + * + * 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.diffplug.gradle.spotless; + +import java.util.SortedMap; +import java.util.TreeMap; + +import org.gradle.api.Action; +import org.gradle.api.Project; + +public class SpotlessExtensionPredeclare extends SpotlessExtension { + private final SortedMap toSetup = new TreeMap<>(); + + public SpotlessExtensionPredeclare(Project project, GradleProvisioner.Policy policy) { + super(project); + getRegisterDependenciesTask().getTaskService().get().predeclaredProvisioner = policy.dedupingProvisioner(project); + project.afterEvaluate(unused -> { + toSetup.forEach((name, formatExtension) -> { + for (Action lazyAction : formatExtension.lazyActions) { + lazyAction.execute(formatExtension); + } + getRegisterDependenciesTask().steps.addAll(formatExtension.steps); + }); + }); + } + + @Override + protected void createFormatTasks(String name, FormatExtension formatExtension) { + toSetup.put(name, formatExtension); + } + + @Override + protected void predeclare(GradleProvisioner.Policy policy) { + throw new UnsupportedOperationException("predeclare can't be called from within `" + EXTENSION_PREDECLARE + "`"); + } +} diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java index de016ae6d2..7fc0ea3775 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java @@ -20,10 +20,10 @@ import java.util.HashMap; import java.util.Map; +import javax.annotation.Nullable; import javax.inject.Inject; import org.gradle.api.DefaultTask; -import org.gradle.api.Project; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.model.ObjectFactory; import org.gradle.api.provider.Property; @@ -48,10 +48,19 @@ public abstract class SpotlessTaskService implements BuildService source = Collections.synchronizedMap(new HashMap<>()); private final Map provisioner = Collections.synchronizedMap(new HashMap<>()); - Provisioner provisionerFor(Project project) { - return provisioner.computeIfAbsent(project.getPath(), unused -> { - return GradleProvisioner.newDedupingProvisioner(project); - }); + @Nullable + GradleProvisioner.DedupingProvisioner predeclaredProvisioner; + + Provisioner provisionerFor(SpotlessExtension spotless) { + if (spotless instanceof SpotlessExtensionPredeclare) { + return predeclaredProvisioner; + } else { + if (predeclaredProvisioner != null) { + return predeclaredProvisioner.cachedOnly; + } else { + return provisioner.computeIfAbsent(spotless.project.getPath(), unused -> new GradleProvisioner.DedupingProvisioner(GradleProvisioner.forProject(spotless.project))); + } + } } void registerSourceAlreadyRan(SpotlessTask task) { diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/MultiProjectTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/MultiProjectTest.java new file mode 100644 index 0000000000..086a2ddb1d --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/MultiProjectTest.java @@ -0,0 +1,154 @@ +/* + * Copyright 2016-2021 DiffPlug + * + * 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.diffplug.gradle.spotless; + +import java.io.IOException; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.diffplug.common.base.StringPrinter; + +class MultiProjectTest extends GradleIntegrationHarness { + private static int N = 100; + + private void createNSubprojects() throws IOException { + for (int i = 0; i < N; ++i) { + createSubproject(Integer.toString(i)); + } + String settings = StringPrinter.buildString(printer -> { + for (int i = 0; i < N; ++i) { + printer.println("include '" + i + "'"); + } + }); + setFile("settings.gradle").toContent(settings); + } + + void createSubproject(String name) throws IOException { + setFile(name + "/build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "", + "spotless {", + " java {", + " target file('test.java')", + " googleJavaFormat('1.2')", + " }", + "}"); + setFile(name + "/test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); + } + + @Test + public void noRootSpotless() throws IOException { + createNSubprojects(); + setFile("build.gradle").toLines(); + applyIsUpToDate(false); + } + + @Test + public void hasRootSpotless() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "", + "spotless {", + " java {", + " target file('test.java')", + " googleJavaFormat('1.2')", + " }", + "}"); + setFile("test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); + createNSubprojects(); + applyIsUpToDate(false); + } + + @Test + public void predeclaredFails() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "spotless { predeclareDeps() }"); + createNSubprojects(); + Assertions.assertThat(gradleRunner().withArguments("spotlessApply").buildAndFail().getOutput()) + .contains("Add a step with [com.google.googlejavaformat:google-java-format:1.2] into the `spotlessPredeclare` block in the root project."); + } + + @Test + public void predeclaredSucceeds() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless { predeclareDeps() }", + "spotlessPredeclare {", + " java { googleJavaFormat('1.2') }", + "}"); + createNSubprojects(); + gradleRunner().withArguments("spotlessApply").build(); + } + + @Test + public void predeclaredFromBuildscriptSucceeds() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless { predeclareDepsFromBuildscript() }", + "spotlessPredeclare {", + " java { googleJavaFormat('1.2') }", + "}"); + createNSubprojects(); + gradleRunner().withArguments("spotlessApply").build(); + } + + @Test + public void predeclaredOrdering() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotlessPredeclare {", + " java { googleJavaFormat('1.2') }", + "}", + "spotless { predeclareDepsFromBuildscript() }"); + createNSubprojects(); + Assertions.assertThat(gradleRunner().withArguments("spotlessApply").buildAndFail().getOutput()) + .contains("Could not find method spotlessPredeclare() for arguments"); + } + + @Test + public void predeclaredUndeclared() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotlessPredeclare {", + " java { googleJavaFormat('1.2') }", + "}"); + createNSubprojects(); + Assertions.assertThat(gradleRunner().withArguments("spotlessApply").buildAndFail().getOutput()) + .contains("Could not find method spotlessPredeclare() for arguments"); + } +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/UpToDateTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/UpToDateTest.java index a0026d2625..9b7eccdfff 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/UpToDateTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/UpToDateTest.java @@ -88,8 +88,8 @@ void testPathologicalCase() throws IOException { // the format task is UP-TO-DATE (same inputs), but the apply tasks will run again pauseForFilesystem(); BuildResult buildResult = gradleRunner().withArguments("spotlessApply").build(); - Assertions.assertThat(buildResult.taskPaths(TaskOutcome.UP_TO_DATE)).containsExactly(":spotlessMisc"); - Assertions.assertThat(buildResult.taskPaths(TaskOutcome.SUCCESS)).containsExactly(":spotlessInternalRegisterDependencies", ":spotlessMiscApply", ":spotlessApply"); + Assertions.assertThat(buildResult.taskPaths(TaskOutcome.UP_TO_DATE)).containsExactly(":spotlessInternalRegisterDependencies", ":spotlessMisc"); + Assertions.assertThat(buildResult.taskPaths(TaskOutcome.SUCCESS)).containsExactly(":spotlessMiscApply", ":spotlessApply"); assertFile("README.md").hasContent("abc"); // and it'll take two more runs to get to fully UP-TO-DATE diff --git a/settings.gradle b/settings.gradle index 02f4e7db8f..ea16d5e1bc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,6 +13,8 @@ pluginManagement { id 'com.diffplug.p2.asmaven' version '3.27.0' // DO NOT UPDATE, see https://github.com/diffplug/spotless/pull/874 // https://github.com/gradle/test-retry-gradle-plugin/releases id 'org.gradle.test-retry' version '1.3.1' + // https://github.com/radarsh/gradle-test-logger-plugin/blob/develop/CHANGELOG.md + id 'com.adarshr.test-logger' version '3.1.0' } } plugins { @@ -23,6 +25,7 @@ plugins { id 'com.diffplug.spotless-changelog' apply false id 'com.diffplug.p2.asmaven' apply false id 'org.gradle.test-retry' apply false + id 'com.adarshr.test-logger' apply false } if (System.env['CI'] != null) { // use the remote buildcache on all CI builds