Skip to content

Commit f18e3d4

Browse files
committed
Test the native image with repositories in the subprojects only
NativeImageResolutionTest builds what #20 reports: the root project declares no repository, the subprojects declare theirs, and openjavaformat.native.formatter is on. formatDiff fails, alone and in parallel, with "Cannot resolve external dependency dev.openjavaformat:open-java-format-native:1.0 because no repositories are defined": the root project resolves palantirJavaFormatNative. The image is a shell script that answers formatDiff with the replacements that format the file, in a Maven repository the test writes under build/, so that the test runs in every build on Linux and macOS, offline; the build has no --add-exports, so only the image could have formatted the file. The test also expects the image to run from a copy in open-java-format-caches, made executable.
1 parent d4a1480 commit f18e3d4

1 file changed

Lines changed: 228 additions & 0 deletions

File tree

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
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.fasterxml.jackson.databind.json.JsonMapper;
20+
import com.fasterxml.jackson.datatype.guava.GuavaModule;
21+
import com.palantir.javaformat.gradle.testing.GradleTestProject;
22+
import com.palantir.javaformat.java.Replacement;
23+
import java.io.IOException;
24+
import java.io.UncheckedIOException;
25+
import java.nio.charset.StandardCharsets;
26+
import java.nio.file.Files;
27+
import java.nio.file.Path;
28+
import java.nio.file.attribute.PosixFilePermissions;
29+
import java.util.List;
30+
import java.util.stream.Stream;
31+
import org.gradle.testkit.runner.BuildResult;
32+
import org.junit.jupiter.api.Test;
33+
import org.junit.jupiter.api.condition.DisabledOnOs;
34+
import org.junit.jupiter.api.condition.OS;
35+
import org.junit.jupiter.api.io.TempDir;
36+
import org.junit.jupiter.params.Parameter;
37+
import org.junit.jupiter.params.ParameterizedClass;
38+
import org.junit.jupiter.params.provider.ValueSource;
39+
40+
/**
41+
* The native image resolves through the repositories of the project that formats, as #20 asks: a build that declares
42+
* its repositories in the subprojects only, as it may for a mirror of Maven Central, formats with it.
43+
*
44+
* <p>The image is a shell script that stands in for open-java-format-native: it answers formatDiff with the
45+
* replacements that format Main.java. The build has no --add-exports, so the formatter inside the plugin could not
46+
* have formatted it; the native jobs of CI run the real image through PalantirJavaFormatPluginTest.
47+
*/
48+
@ParameterizedClass(name = "configuration cache {0}")
49+
@ValueSource(booleans = {true, false})
50+
@DisabledOnOs(value = OS.WINDOWS, disabledReason = "The stand-in for the native image is a shell script")
51+
final class NativeImageResolutionTest {
52+
53+
/**
54+
* A Maven repository with dev.openjavaformat:open-java-format-native:1.0 for this platform. Under build/, as the
55+
* other repositories of the tests are: the daemon may still run the image after a test.
56+
*/
57+
private static final Path NATIVE_REPO = Path.of("build/native-repo");
58+
59+
private static final String VERSION = "1.0";
60+
private static final String MAIN_JAVA = "src/main/java/Main.java";
61+
private static final String UNFORMATTED = "class Main {\n int x;\n}\n";
62+
private static final String FORMATTED = "class Main {\n int x;\n}\n";
63+
64+
@Parameter
65+
private boolean configurationCache;
66+
67+
@TempDir
68+
private Path projectDir;
69+
70+
@Test
71+
void formatDiff_runs_the_native_image_from_the_repositories_of_the_subprojects() throws Exception {
72+
var project = project();
73+
commitThenChange(project, "a/" + MAIN_JAVA, "b/" + MAIN_JAVA);
74+
75+
var result = project.succeeds("formatDiff", "--info");
76+
77+
assertThat(result.getOutput()).contains("Using the native-image formatter");
78+
assertFormatted(project, "a/" + MAIN_JAVA, "b/" + MAIN_JAVA);
79+
assertRunFromTheCaches(result);
80+
}
81+
82+
@Test
83+
void formatDiff_runs_the_native_image_in_a_parallel_build() throws Exception {
84+
var project = project();
85+
commitThenChange(project, "a/" + MAIN_JAVA, "b/" + MAIN_JAVA);
86+
87+
project.succeeds("formatDiff", "--parallel");
88+
89+
assertFormatted(project, "a/" + MAIN_JAVA, "b/" + MAIN_JAVA);
90+
}
91+
92+
/**
93+
* The layout of the docs, with the repositories in the subprojects only, and the image declared in the root project
94+
* as a release's would be by default. Prints the Gradle user home, where the image is copied to.
95+
*/
96+
private GradleTestProject project() throws IOException {
97+
var platform = currentPlatform();
98+
writeNativeImage(platform);
99+
return new GradleTestProject(projectDir)
100+
.configurationCache(configurationCache)
101+
.withoutRepositories()
102+
.pluginsNotApplied("dev.openjavaformat.java-format")
103+
.subprojects("a", "b")
104+
.gradleProperties("openjavaformat.native.formatter=true")
105+
.buildGradle(
106+
"""
107+
subprojects {
108+
apply plugin: 'java'
109+
apply plugin: 'dev.openjavaformat.java-format'
110+
repositories {
111+
maven { url = rootProject.file("%s") }
112+
}
113+
}
114+
dependencies {
115+
palantirJavaFormatNative 'dev.openjavaformat:open-java-format-native:%s:%s@%s'
116+
}
117+
println "gradleUserHome=${gradle.gradleUserHomeDir}"
118+
""",
119+
NATIVE_REPO.toAbsolutePath().toString().replace('\\', '/'),
120+
VERSION,
121+
platform.classifier(),
122+
platform.extension());
123+
}
124+
125+
private static void assertFormatted(GradleTestProject project, String... files) {
126+
for (var file : files) {
127+
assertThat(project.readFile(file)).as(file).isEqualTo(FORMATTED);
128+
}
129+
}
130+
131+
/** The build runs the image from a copy of its own, made executable: the repository's file may not be. */
132+
private static void assertRunFromTheCaches(BuildResult result) throws IOException {
133+
var gradleUserHome = result.getOutput()
134+
.lines()
135+
.filter(line -> line.startsWith("gradleUserHome="))
136+
.map(line -> Path.of(line.substring("gradleUserHome=".length())))
137+
.findFirst()
138+
.orElseThrow();
139+
try (Stream<Path> files = Files.walk(gradleUserHome.resolve(BundledFormatter.CACHES))) {
140+
assertThat(files.filter(file -> file.getFileName().toString().startsWith("open-java-format-native-"))
141+
.toList())
142+
.isNotEmpty()
143+
.allSatisfy(image -> assertThat(Files.isExecutable(image)).isTrue());
144+
}
145+
}
146+
147+
private static NativePlatform currentPlatform() {
148+
return NativePlatform.of(
149+
System.getProperty("os.name"), System.getProperty("os.arch"), NativeImageResolutionTest::lddVersion);
150+
}
151+
152+
private static String lddVersion() {
153+
try {
154+
var process = new ProcessBuilder("ldd", "--version")
155+
.redirectErrorStream(true)
156+
.start();
157+
var output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
158+
process.waitFor();
159+
return output.lines().findFirst().orElse("");
160+
} catch (IOException e) {
161+
throw new UncheckedIOException(e);
162+
} catch (InterruptedException e) {
163+
Thread.currentThread().interrupt();
164+
throw new IllegalStateException(e);
165+
}
166+
}
167+
168+
/**
169+
* open-java-format-native as Maven Central has it, a POM and the image under the platform's classifier, with a
170+
* script for the image: it reads the source formatDiff sends, and answers with the replacements that format it.
171+
*/
172+
private static void writeNativeImage(NativePlatform platform) throws IOException {
173+
var dir = Files.createDirectories(NATIVE_REPO.resolve("dev/openjavaformat/open-java-format-native/" + VERSION));
174+
Files.writeString(dir.resolve("open-java-format-native-" + VERSION + ".pom"), """
175+
<?xml version="1.0" encoding="UTF-8"?>
176+
<project xmlns="http://maven.apache.org/POM/4.0.0">
177+
<modelVersion>4.0.0</modelVersion>
178+
<groupId>dev.openjavaformat</groupId>
179+
<artifactId>open-java-format-native</artifactId>
180+
<version>%s</version>
181+
</project>
182+
""".formatted(VERSION));
183+
var replacements = JsonMapper.builder()
184+
.addModule(new GuavaModule())
185+
.build()
186+
.writeValueAsString(List.of(Replacement.create(0, UNFORMATTED.length(), FORMATTED)));
187+
var image = dir.resolve(
188+
"open-java-format-native-" + VERSION + "-" + platform.classifier() + "." + platform.extension());
189+
Files.writeString(image, """
190+
#!/bin/sh
191+
cat > /dev/null
192+
cat <<'JSON'
193+
%s
194+
JSON
195+
""".formatted(replacements));
196+
Files.setPosixFilePermissions(image, PosixFilePermissions.fromString("rw-r--r--"));
197+
}
198+
199+
/** Commits the files in a formatted form, then writes them unformatted: formatDiff formats what git shows changed. */
200+
private static void commitThenChange(GradleTestProject project, String... files)
201+
throws IOException, InterruptedException {
202+
for (var file : files) {
203+
project.writeFile(file, "class Main {}\n");
204+
}
205+
git(project, "init");
206+
git(project, "config", "user.name", "Foo");
207+
git(project, "config", "user.email", "foo@bar.com");
208+
// The repository this runs in may sign commits; a throwaway one has no key to sign with.
209+
git(project, "config", "commit.gpgsign", "false");
210+
git(project, "add", ".");
211+
git(project, "commit", "-m", "Commit");
212+
for (var file : files) {
213+
project.writeFile(file, UNFORMATTED);
214+
}
215+
}
216+
217+
private static void git(GradleTestProject project, String... args) throws IOException, InterruptedException {
218+
var command = new String[args.length + 1];
219+
command[0] = "git";
220+
System.arraycopy(args, 0, command, 1, args.length);
221+
var process =
222+
new ProcessBuilder(command).directory(project.path().toFile()).start();
223+
var exitCode = process.waitFor();
224+
if (exitCode != 0) {
225+
throw new RuntimeException("git " + String.join(" ", args) + " failed with exit code " + exitCode);
226+
}
227+
}
228+
}

0 commit comments

Comments
 (0)