Skip to content

Commit 3b720d5

Browse files
committed
Test the plugin with and without the configuration cache
ConfigurationCacheTest runs formatDiff, spotlessApply, spotlessCheck and the IntelliJ tasks twice with the cache on: the first build stores the entry, the second reuses it, and both do the work. Until now the only test with the cache on ran classes, which creates the Spotless step but never formats with it. GradleTestProject turns the cache on by default, and the TestKit test classes run both ways through @ParameterizedClass, in the same test task, so CI runs them unchanged. The builds also run without file system watching: a test changes a file faster than the events arrive, and the next build took a deleted output for one still there. Tests on JDK 21: 68 in :gradle-open-java-format on Gradle 9.3.1 and on 9.8.0, 83 with -PnativeImage=true; ./gradlew test passes.
1 parent 47da253 commit 3b720d5

10 files changed

Lines changed: 338 additions & 100 deletions

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,28 @@
2323
import org.gradle.testkit.runner.BuildResult;
2424
import org.junit.jupiter.api.Test;
2525
import org.junit.jupiter.api.io.TempDir;
26+
import org.junit.jupiter.params.Parameter;
27+
import org.junit.jupiter.params.ParameterizedClass;
28+
import org.junit.jupiter.params.provider.ValueSource;
2629

2730
/**
2831
* Most projects that apply the formatter never apply Spotless, so there are no Spotless classes on their buildscript
2932
* classpath. The Spotless integration has to stay dormant there instead of failing the whole plugin.
3033
*/
34+
@ParameterizedClass(name = "configuration cache {0}")
35+
@ValueSource(booleans = {true, false})
3136
final class AppliesWithoutSpotlessTest {
3237

38+
@Parameter
39+
private boolean configurationCache;
40+
3341
@TempDir
3442
private Path projectDir;
3543

3644
@Test
3745
void applies_without_spotless_on_the_classpath() {
3846
GradleTestProject project = new GradleTestProject(projectDir)
47+
.configurationCache(configurationCache)
3948
.plugins("java", "dev.openjavaformat.java-format")
4049
.withoutSpotlessOnClasspath();
4150

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
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.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.util.List;
25+
import java.util.jar.JarEntry;
26+
import java.util.jar.JarOutputStream;
27+
import org.gradle.testkit.runner.BuildResult;
28+
import org.gradle.testkit.runner.BuildTask;
29+
import org.gradle.testkit.runner.TaskOutcome;
30+
import org.junit.jupiter.api.Test;
31+
import org.junit.jupiter.api.io.TempDir;
32+
33+
/**
34+
* Runs each task twice with the configuration cache, as a build that turns the cache on does: the first run stores the
35+
* entry, and already runs its tasks from it, the second run reuses it. formatDiff used to call Task.project when it
36+
* ran, and the Spotless step came back from the cache without its jars and its formatter (#193).
37+
*/
38+
final class ConfigurationCacheTest {
39+
40+
/** ./gradlew writeImplClasspath generates this file. */
41+
private static final Path CLASSPATH_FILE = Path.of("build/impl.classpath");
42+
43+
private static final String STORED = "Configuration cache entry stored.";
44+
private static final String REUSED = "Configuration cache entry reused.";
45+
46+
private static final String MAIN_JAVA = "src/main/java/Main.java";
47+
private static final String UNFORMATTED = "class Main {\n int x;\n}\n";
48+
private static final String FORMATTED = "class Main {\n int x;\n}\n";
49+
50+
@TempDir
51+
private Path projectDir;
52+
53+
@Test
54+
void formatDiff_formats_from_a_reused_entry() throws IOException, InterruptedException {
55+
var project = project("java", "dev.openjavaformat.java-format");
56+
git(project, "init");
57+
git(project, "config", "user.name", "Foo");
58+
git(project, "config", "user.email", "foo@bar.com");
59+
// The repository this runs in may sign commits; a throwaway one has no key to sign with.
60+
git(project, "config", "commit.gpgsign", "false");
61+
project.writeFile(MAIN_JAVA, "class Main {}\n");
62+
git(project, "add", ".");
63+
git(project, "commit", "-m", "Commit");
64+
65+
for (var entry : List.of(STORED, REUSED)) {
66+
project.writeFile(MAIN_JAVA, UNFORMATTED);
67+
68+
assertThat(project.succeeds("formatDiff").getOutput()).contains(entry);
69+
assertThat(project.readFile(MAIN_JAVA)).isEqualTo(FORMATTED);
70+
}
71+
}
72+
73+
@Test
74+
void spotlessApply_formats_from_a_reused_entry() throws IOException {
75+
var project = project("java", "dev.openjavaformat.java-format", "com.diffplug.spotless");
76+
77+
for (var entry : List.of(STORED, REUSED)) {
78+
project.writeFile(MAIN_JAVA, UNFORMATTED);
79+
80+
assertThat(project.succeeds("spotlessApply").getOutput()).contains(entry);
81+
// Spotless writes the platform's line endings, CRLF on Windows.
82+
assertThat(project.readFile(MAIN_JAVA)).isEqualToNormalizingNewlines(FORMATTED);
83+
}
84+
}
85+
86+
@Test
87+
void spotlessCheck_reports_the_violation_from_a_reused_entry() throws IOException {
88+
var project = project("java", "dev.openjavaformat.java-format", "com.diffplug.spotless")
89+
.writeFile(MAIN_JAVA, UNFORMATTED);
90+
91+
for (var entry : List.of(STORED, REUSED)) {
92+
// A formatter that fails reports a lint error instead, which is how the lost formatter showed.
93+
assertThat(project.fails("spotlessCheck").getOutput())
94+
.contains(entry)
95+
.contains("to fix all violations")
96+
.doesNotContain("lint error");
97+
}
98+
}
99+
100+
@Test
101+
void spotlessJava_is_up_to_date_until_the_formatter_jars_change() throws IOException {
102+
var jars = Files.readString(CLASSPATH_FILE).trim();
103+
var project = new GradleTestProject(projectDir)
104+
.configurationCache(true)
105+
.plugins("java", "dev.openjavaformat.java-format", "com.diffplug.spotless")
106+
.withJavacInternalExports()
107+
.writeFile("formatter.classpath", jars)
108+
.buildGradle("""
109+
dependencies {
110+
palantirJavaFormat files(file("formatter.classpath").text.split(File.pathSeparator))
111+
}
112+
""")
113+
.writeFile(MAIN_JAVA, FORMATTED);
114+
115+
assertThat(spotlessJava(project.succeeds("spotlessCheck"))).isEqualTo(TaskOutcome.SUCCESS);
116+
var unchanged = project.succeeds("spotlessCheck");
117+
assertThat(unchanged.getOutput()).contains(REUSED);
118+
assertThat(spotlessJava(unchanged)).isEqualTo(TaskOutcome.UP_TO_DATE);
119+
120+
// Spotless signs the jars by name, size and content, as another formatter version changes them.
121+
var extraJar = projectDir.resolve("extra.jar");
122+
try (var jar = new JarOutputStream(Files.newOutputStream(extraJar))) {
123+
jar.putNextEntry(new JarEntry("extra.txt"));
124+
}
125+
project.writeFile("formatter.classpath", jars + File.pathSeparator + extraJar);
126+
127+
assertThat(spotlessJava(project.succeeds("spotlessCheck"))).isEqualTo(TaskOutcome.SUCCESS);
128+
}
129+
130+
@Test
131+
void idea_tasks_run_from_a_reused_entry() {
132+
var project = project("java", "dev.openjavaformat.java-format", "idea");
133+
134+
for (var entry : List.of(STORED, REUSED)) {
135+
project.file(".idea/open-java-format.xml").toFile().delete();
136+
137+
// Not `idea`: Gradle's own idea tasks are not compatible with the configuration cache, and running
138+
// them turns it off. The plugin appends its tasks to every build, so `help` runs them too.
139+
var result = project.succeeds("help");
140+
141+
assertThat(result.getOutput()).contains(entry);
142+
assertThat(result.task(":updateOpenJavaFormatXml"))
143+
.isNotNull()
144+
.extracting(BuildTask::getOutcome)
145+
.isEqualTo(TaskOutcome.SUCCESS);
146+
assertThat(project.file(".idea/open-java-format.xml")).exists();
147+
}
148+
}
149+
150+
@Test
151+
void required_idea_plugin_is_listed_from_a_reused_entry() {
152+
var project = project("java", "dev.openjavaformat.java-format");
153+
154+
for (var entry : List.of(STORED, REUSED)) {
155+
project.file(".idea/externalDependencies.xml").toFile().delete();
156+
157+
assertThat(project.succeeds("help", "-Didea.active=true").getOutput())
158+
.contains(entry);
159+
assertThat(project.file(".idea/externalDependencies.xml")).exists();
160+
}
161+
}
162+
163+
private GradleTestProject project(String... plugins) {
164+
// Forward slashes: the path goes into a Groovy string, where a Windows backslash would start an escape.
165+
return new GradleTestProject(projectDir)
166+
.configurationCache(true)
167+
.plugins(plugins)
168+
.withJavacInternalExports()
169+
.buildGradle("""
170+
dependencies {
171+
palantirJavaFormat files(file("%s").text.split(File.pathSeparator))
172+
}
173+
""", CLASSPATH_FILE.toAbsolutePath().toString().replace('\\', '/'));
174+
}
175+
176+
private static TaskOutcome spotlessJava(BuildResult result) {
177+
var task = result.task(":spotlessJava");
178+
assertThat(task).isNotNull();
179+
return task.getOutcome();
180+
}
181+
182+
private static void git(GradleTestProject project, String... args) throws IOException, InterruptedException {
183+
var command = new String[args.length + 1];
184+
command[0] = "git";
185+
System.arraycopy(args, 0, command, 1, args.length);
186+
var process =
187+
new ProcessBuilder(command).directory(project.path().toFile()).start();
188+
var exitCode = process.waitFor();
189+
if (exitCode != 0) {
190+
throw new RuntimeException("git " + String.join(" ", args) + " failed with exit code " + exitCode);
191+
}
192+
}
193+
}

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,17 @@
2424
import org.gradle.testkit.runner.BuildResult;
2525
import org.junit.jupiter.api.Test;
2626
import org.junit.jupiter.api.io.TempDir;
27+
import org.junit.jupiter.params.Parameter;
28+
import org.junit.jupiter.params.ParameterizedClass;
29+
import org.junit.jupiter.params.provider.ValueSource;
2730

2831
/**
2932
* A build without the JVM flags used to fail on the first file with a raw IllegalAccessError from the formatter's
3033
* internals (#19). It fails before the first file now, naming the two settings that make the formatter run. This
3134
* class runs in the default test task; PalantirJavaFormatPluginTest does not, because it needs the native binary.
3235
*/
36+
@ParameterizedClass(name = "configuration cache {0}")
37+
@ValueSource(booleans = {true, false})
3338
final class FormatDiffWithoutJavacExportsTest {
3439

3540
/** ./gradlew writeImplClasspath generates this file. Forward slashes: the path goes into a Groovy string. */
@@ -38,21 +43,23 @@ final class FormatDiffWithoutJavacExportsTest {
3843

3944
private static final String MAIN_JAVA = "src/main/java/Main.java";
4045

46+
@Parameter
47+
private boolean configurationCache;
48+
4149
@TempDir
4250
private Path projectDir;
4351

4452
@Test
4553
void formatDiff_names_both_settings_when_the_gradle_jvm_does_not_export_javac()
4654
throws IOException, InterruptedException {
4755
GradleTestProject project = new GradleTestProject(projectDir)
56+
.configurationCache(configurationCache)
4857
.plugins("java", "dev.openjavaformat.java-format")
49-
.buildGradle(
50-
"""
51-
dependencies {
52-
palantirJavaFormat files(file("%s").text.split(File.pathSeparator))
53-
}
54-
""",
55-
CLASSPATH_FILE);
58+
.buildGradle("""
59+
dependencies {
60+
palantirJavaFormat files(file("%s").text.split(File.pathSeparator))
61+
}
62+
""", CLASSPATH_FILE);
5663

5764
git(project, "init");
5865
git(project, "config", "user.name", "Foo");

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

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,13 @@
3232
import org.gradle.testkit.runner.BuildTask;
3333
import org.gradle.testkit.runner.TaskOutcome;
3434
import org.junit.jupiter.api.io.TempDir;
35+
import org.junit.jupiter.params.Parameter;
36+
import org.junit.jupiter.params.ParameterizedClass;
3537
import org.junit.jupiter.params.ParameterizedTest;
3638
import org.junit.jupiter.params.provider.ValueSource;
3739

40+
@ParameterizedClass(name = "configuration cache {0}")
41+
@ValueSource(booleans = {true, false})
3842
final class PalantirJavaFormatIdeaPluginTest {
3943

4044
// Forward slashes: the path goes into a Groovy string, where a Windows backslash would start an escape.
@@ -46,23 +50,25 @@ final class PalantirJavaFormatIdeaPluginTest {
4650

4751
private static final ObjectMapper XML_MAPPER = new XmlMapper();
4852

53+
@Parameter
54+
private boolean configurationCache;
55+
4956
@TempDir
5057
private Path projectDir;
5158

5259
@ParameterizedTest(name = "extraGradleProperties={0}")
5360
@ValueSource(strings = {"", "openjavaformat.native.formatter=true"})
5461
void idea_configures_xml_files(String extraGradleProperties) throws IOException {
5562
GradleTestProject project = new GradleTestProject(projectDir)
63+
.configurationCache(configurationCache)
5664
.plugins("dev.openjavaformat.java-format-idea", "idea")
5765
.gradleProperties(extraGradleProperties)
58-
.buildGradle(
59-
"""
60-
dependencies {
61-
palantirJavaFormat project.files() // no need to store the real thing in here
62-
%s
63-
}
64-
""",
65-
extraGradleProperties.isBlank() ? "" : NATIVE_CONFIG);
66+
.buildGradle("""
67+
dependencies {
68+
palantirJavaFormat project.files() // no need to store the real thing in here
69+
%s
70+
}
71+
""", extraGradleProperties.isBlank() ? "" : NATIVE_CONFIG);
6672

6773
BuildResult result = project.succeeds("idea");
6874

0 commit comments

Comments
 (0)