Skip to content

Commit 6b1cd06

Browse files
committed
Test spotlessJava with the build cache
No test ran a build with the build cache on, so nothing showed that spotlessJava is taken from it only when it should be. BuildCacheTest does, with the configuration cache and without: spotlessJava is up to date when the source is written again unchanged, runs when the source or the formatter's jars change, and comes from the cache when the source changes back, after the build directory is deleted and in a copy of the project in another directory. Each build is spotlessApply, and each test checks the file it leaves. That shows the output spotlessJava ran for, kept or took from the cache is the source formatted; a step that changes nothing passes a spotlessCheck of a formatted file as well. GradleTestProject can now run its builds with the build cache, in a directory of the test's own: TestKit builds share one Gradle user home, and with it the default local cache. It can also delete a file or a directory of the project.
1 parent 44e282b commit 6b1cd06

2 files changed

Lines changed: 197 additions & 1 deletion

File tree

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
package com.palantir.javaformat.gradle;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
19+
import com.palantir.javaformat.gradle.testing.GradleTestProject;
20+
import java.io.File;
21+
import java.io.IOException;
22+
import java.net.URISyntaxException;
23+
import java.nio.file.Files;
24+
import java.nio.file.Path;
25+
import org.gradle.testkit.runner.TaskOutcome;
26+
import org.junit.jupiter.api.Test;
27+
import org.junit.jupiter.api.io.TempDir;
28+
import org.junit.jupiter.params.Parameter;
29+
import org.junit.jupiter.params.ParameterizedClass;
30+
import org.junit.jupiter.params.provider.ValueSource;
31+
32+
/**
33+
* spotlessJava is cacheable, and the open-java-format step is part of its cache key: the key changes with the
34+
* formatter's jars, and not with the directory the project is in.
35+
*
36+
* <p>Every build here is spotlessApply, after the source is written again. The formatted file is what shows that
37+
* spotlessJava's output, whether it ran, was up to date or came from the cache, holds the source formatted: a step that
38+
* leaves the source as it is passes a spotlessCheck of a formatted file just as well. Writing the same source again is
39+
* the change that must not count, as only its time changes.
40+
*/
41+
@ParameterizedClass(name = "configuration cache {0}")
42+
@ValueSource(booleans = {true, false})
43+
final class BuildCacheTest {
44+
45+
/** ./gradlew writeImplClasspath generates this file. */
46+
private static final Path CLASSPATH_FILE = Path.of("build/impl.classpath");
47+
48+
private static final String MAIN_JAVA = "src/main/java/Main.java";
49+
private static final String UNFORMATTED = "class Main {\n int x;\n}\n";
50+
private static final String FORMATTED = "class Main {\n int x;\n}\n";
51+
private static final String OTHER_UNFORMATTED = "class Main {\n int y;\n}\n";
52+
private static final String OTHER_FORMATTED = "class Main {\n int y;\n}\n";
53+
54+
@Parameter
55+
private boolean configurationCache;
56+
57+
@TempDir
58+
private Path projectDir;
59+
60+
@TempDir
61+
private Path otherProjectDir;
62+
63+
@TempDir
64+
private Path buildCacheDir;
65+
66+
@Test
67+
void spotlessJava_is_up_to_date_when_the_source_is_written_again_unchanged() throws IOException {
68+
var project = project(projectDir);
69+
assertThat(spotlessApply(project, UNFORMATTED)).isEqualTo(TaskOutcome.SUCCESS);
70+
assertThat(project.readFile(MAIN_JAVA)).isEqualToNormalizingNewlines(FORMATTED);
71+
72+
assertThat(spotlessApply(project, UNFORMATTED)).isEqualTo(TaskOutcome.UP_TO_DATE);
73+
assertThat(project.readFile(MAIN_JAVA)).isEqualToNormalizingNewlines(FORMATTED);
74+
}
75+
76+
@Test
77+
void spotlessJava_runs_again_when_the_source_changes() throws IOException {
78+
var project = project(projectDir);
79+
assertThat(spotlessApply(project, UNFORMATTED)).isEqualTo(TaskOutcome.SUCCESS);
80+
81+
assertThat(spotlessApply(project, OTHER_UNFORMATTED)).isEqualTo(TaskOutcome.SUCCESS);
82+
assertThat(project.readFile(MAIN_JAVA)).isEqualToNormalizingNewlines(OTHER_FORMATTED);
83+
}
84+
85+
@Test
86+
void spotlessJava_comes_from_the_cache_when_the_source_changes_back() throws IOException {
87+
var project = project(projectDir);
88+
assertThat(spotlessApply(project, UNFORMATTED)).isEqualTo(TaskOutcome.SUCCESS);
89+
assertThat(spotlessApply(project, OTHER_UNFORMATTED)).isEqualTo(TaskOutcome.SUCCESS);
90+
91+
assertThat(spotlessApply(project, UNFORMATTED)).isEqualTo(TaskOutcome.FROM_CACHE);
92+
assertThat(project.readFile(MAIN_JAVA)).isEqualToNormalizingNewlines(FORMATTED);
93+
}
94+
95+
@Test
96+
void spotlessJava_comes_from_the_cache_after_the_build_directory_is_deleted() throws IOException {
97+
var project = project(projectDir);
98+
assertThat(spotlessApply(project, UNFORMATTED)).isEqualTo(TaskOutcome.SUCCESS);
99+
100+
project.delete("build");
101+
102+
assertThat(spotlessApply(project, UNFORMATTED)).isEqualTo(TaskOutcome.FROM_CACHE);
103+
assertThat(project.readFile(MAIN_JAVA)).isEqualToNormalizingNewlines(FORMATTED);
104+
}
105+
106+
@Test
107+
void spotlessJava_comes_from_the_cache_in_another_directory() throws IOException {
108+
assertThat(spotlessApply(project(projectDir), UNFORMATTED)).isEqualTo(TaskOutcome.SUCCESS);
109+
110+
// The same project somewhere else, as another checkout or another CI machine has it.
111+
var moved = project(otherProjectDir);
112+
113+
assertThat(spotlessApply(moved, UNFORMATTED)).isEqualTo(TaskOutcome.FROM_CACHE);
114+
assertThat(moved.readFile(MAIN_JAVA)).isEqualToNormalizingNewlines(FORMATTED);
115+
}
116+
117+
@Test
118+
void spotlessJava_runs_again_when_the_formatter_jars_change() throws IOException, URISyntaxException {
119+
var project = project(projectDir);
120+
assertThat(spotlessApply(project, UNFORMATTED)).isEqualTo(TaskOutcome.SUCCESS);
121+
122+
// Spotless signs the jars by name, size and content, as another formatter version changes them. The extra jar
123+
// is JUnit's, not one written into projectDir: the daemon keeps the formatter's jars open after the build, and
124+
// Windows does not delete an open file. Without the build directory, only the cache could skip the task.
125+
var extraJar = Path.of(
126+
Test.class.getProtectionDomain().getCodeSource().getLocation().toURI());
127+
project.writeFile("formatter.classpath", formatterClasspath() + File.pathSeparator + extraJar)
128+
.delete("build");
129+
130+
assertThat(spotlessApply(project, UNFORMATTED)).isEqualTo(TaskOutcome.SUCCESS);
131+
assertThat(project.readFile(MAIN_JAVA)).isEqualToNormalizingNewlines(FORMATTED);
132+
}
133+
134+
private GradleTestProject project(Path dir) throws IOException {
135+
return new GradleTestProject(dir)
136+
.configurationCache(configurationCache)
137+
.withBuildCache(buildCacheDir)
138+
.plugins("java", "dev.openjavaformat.java-format", "com.diffplug.spotless")
139+
.withJavacInternalExports()
140+
.writeFile("formatter.classpath", formatterClasspath())
141+
.buildGradle("""
142+
dependencies {
143+
palantirJavaFormat files(file("formatter.classpath").text.split(File.pathSeparator))
144+
}
145+
""");
146+
}
147+
148+
private static String formatterClasspath() throws IOException {
149+
return Files.readString(CLASSPATH_FILE).trim();
150+
}
151+
152+
/** Writes the source and runs spotlessApply; returns what spotlessJava did for it. */
153+
private static TaskOutcome spotlessApply(GradleTestProject project, String source) {
154+
var task =
155+
project.writeFile(MAIN_JAVA, source).succeeds("spotlessApply").task(":spotlessJava");
156+
assertThat(task).isNotNull();
157+
return task.getOutcome();
158+
}
159+
}

‎gradle-open-java-format/src/test/java/com/palantir/javaformat/gradle/testing/GradleTestProject.java‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@
2525
import java.nio.file.Path;
2626
import java.util.ArrayList;
2727
import java.util.Arrays;
28+
import java.util.Comparator;
2829
import java.util.LinkedHashSet;
2930
import java.util.List;
3031
import java.util.Objects;
32+
import java.util.Optional;
3133
import java.util.Properties;
3234
import java.util.Set;
35+
import java.util.stream.Stream;
3336
import org.gradle.testkit.runner.BuildResult;
3437
import org.gradle.testkit.runner.GradleRunner;
3538

@@ -61,6 +64,7 @@ public final class GradleTestProject {
6164
private final StringBuilder gradleProperties = new StringBuilder();
6265
private boolean configurationCache = true;
6366
private boolean spotlessOnClasspath = true;
67+
private Optional<Path> buildCacheDir = Optional.empty();
6468

6569
public GradleTestProject(Path projectDir) {
6670
this.projectDir = projectDir;
@@ -109,6 +113,15 @@ public GradleTestProject configurationCache(boolean enabled) {
109113
return this;
110114
}
111115

116+
/**
117+
* Runs the builds with the build cache, kept in {@code directory}. Not the default local cache: TestKit builds share
118+
* one Gradle user home, and with it that cache, so a test would read what another test stored.
119+
*/
120+
public GradleTestProject withBuildCache(Path directory) {
121+
this.buildCacheDir = Optional.of(directory);
122+
return this;
123+
}
124+
112125
/**
113126
* Leaves the Spotless jars out of the classpath injected next to the plugin under test. build.gradle puts them there
114127
* so that generated projects can apply Spotless, which gives every other test Spotless classes whether it applies
@@ -134,6 +147,22 @@ public GradleTestProject writeFile(String relativePath, String content) {
134147
return this;
135148
}
136149

150+
/** Deletes a file, or a directory with everything in it, if it is there. */
151+
public GradleTestProject delete(String relativePath) {
152+
Path target = file(relativePath);
153+
if (!Files.exists(target)) {
154+
return this;
155+
}
156+
try (Stream<Path> paths = Files.walk(target)) {
157+
for (var path : paths.sorted(Comparator.reverseOrder()).toList()) {
158+
Files.delete(path);
159+
}
160+
} catch (IOException e) {
161+
throw new UncheckedIOException("Could not delete " + relativePath, e);
162+
}
163+
return this;
164+
}
165+
137166
public String readFile(String relativePath) {
138167
try {
139168
return Files.readString(file(relativePath), StandardCharsets.UTF_8);
@@ -158,6 +187,9 @@ private GradleRunner runner(String... arguments) {
158187
// Every build looks at the files afresh. A test changes files between two builds faster than file system
159188
// watching reports it, and the second build then took a deleted output for one still there.
160189
allArguments.add("--no-watch-fs");
190+
if (buildCacheDir.isPresent()) {
191+
allArguments.add("--build-cache");
192+
}
161193
GradleRunner runner = GradleRunner.create()
162194
.withProjectDir(projectDir.toFile())
163195
.withGradleVersion(GRADLE_VERSION)
@@ -186,7 +218,12 @@ private void write() {
186218
plugins.forEach(id -> script.append(" id '").append(id).append("'\n"));
187219
script.append("}\n\nrepositories {\n mavenCentral()\n}\n\n").append(buildGradle);
188220

189-
writeFile("settings.gradle", "rootProject.name = 'test-project'\n");
221+
StringBuilder settings = new StringBuilder("rootProject.name = 'test-project'\n");
222+
// Forward slashes: the path goes into a Groovy string, where a Windows backslash would start an escape.
223+
buildCacheDir.ifPresent(dir -> settings.append("\nbuildCache {\n local {\n directory = file('")
224+
.append(dir.toAbsolutePath().toString().replace('\\', '/'))
225+
.append("')\n }\n}\n"));
226+
writeFile("settings.gradle", settings.toString());
190227
writeFile("build.gradle", script.toString());
191228
writeFile("gradle.properties", gradleProperties.toString());
192229
}

0 commit comments

Comments
 (0)