Skip to content

Commit 5348880

Browse files
committed
Modularize spring-boot-test-autoconfigure
This commit modularizes spring-boot-test-autoconfigure. It now contains only the code that's central to test auto-configuration. Feature-specific functionality has moved out into -test modules, some existing and some newly created. For example, `@DataJpaTest` can now be found in spring-boot-data-jpa-test. Closes gh-47322
1 parent 7979a51 commit 5348880

File tree

767 files changed

+5576
-2209
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

767 files changed

+5576
-2209
lines changed

buildSrc/build.gradle

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,6 @@ gradlePlugin {
133133
id = "org.springframework.boot.integration-test"
134134
implementationClass = "org.springframework.boot.build.test.IntegrationTestPlugin"
135135
}
136-
systemTestPlugin {
137-
id = "org.springframework.boot.system-test"
138-
implementationClass = "org.springframework.boot.build.test.SystemTestPlugin"
139-
}
140136
mavenPluginPlugin {
141137
id = "org.springframework.boot.maven-plugin"
142138
implementationClass = "org.springframework.boot.build.mavenplugin.MavenPluginPlugin"
@@ -153,10 +149,22 @@ gradlePlugin {
153149
id = "org.springframework.boot.starter"
154150
implementationClass = "org.springframework.boot.build.starters.StarterPlugin"
155151
}
152+
systemTestPlugin {
153+
id = "org.springframework.boot.system-test"
154+
implementationClass = "org.springframework.boot.build.test.SystemTestPlugin"
155+
}
156+
testAutoConfigurationPlugin {
157+
id = "org.springframework.boot.test-auto-configuration"
158+
implementationClass = "org.springframework.boot.build.test.autoconfigure.TestAutoConfigurationPlugin"
159+
}
156160
testFailuresPlugin {
157161
id = "org.springframework.boot.test-failures"
158162
implementationClass = "org.springframework.boot.build.testing.TestFailuresPlugin"
159163
}
164+
testSlicePlugin {
165+
id = "org.springframework.boot.test-slice"
166+
implementationClass = "org.springframework.boot.build.test.autoconfigure.TestSlicePlugin"
167+
}
160168
}
161169
}
162170

buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationClass.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.File;
2020
import java.io.FileInputStream;
2121
import java.io.IOException;
22+
import java.io.InputStream;
2223
import java.io.UncheckedIOException;
2324
import java.util.ArrayList;
2425
import java.util.Collections;
@@ -54,8 +55,8 @@ private AutoConfigurationClass(String name, Map<String, List<String>> attributes
5455
attributes.getOrDefault("afterName", Collections.emptyList()));
5556
}
5657

57-
static AutoConfigurationClass of(File classFile) {
58-
try (FileInputStream input = new FileInputStream(classFile)) {
58+
public static AutoConfigurationClass of(InputStream input) {
59+
try {
5960
ClassReader classReader = new ClassReader(input);
6061
AutoConfigurationClassVisitor visitor = new AutoConfigurationClassVisitor();
6162
classReader.accept(visitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES);
@@ -66,6 +67,15 @@ static AutoConfigurationClass of(File classFile) {
6667
}
6768
}
6869

70+
static AutoConfigurationClass of(File classFile) {
71+
try (InputStream input = new FileInputStream(classFile)) {
72+
return of(input);
73+
}
74+
catch (IOException ex) {
75+
throw new UncheckedIOException(ex);
76+
}
77+
}
78+
6979
private static final class AutoConfigurationClassVisitor extends ClassVisitor {
7080

7181
private AutoConfigurationClass autoConfigurationClass;

buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationImportsTask.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@
3838
*/
3939
public abstract class AutoConfigurationImportsTask extends DefaultTask {
4040

41-
static final String IMPORTS_FILE = "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports";
41+
/**
42+
* The path of the {@code AutoConfiguration.imports} file.
43+
*/
44+
public static final String IMPORTS_FILE = "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports";
4245

4346
private FileCollection sourceFiles = getProject().getObjects().fileCollection();
4447

buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesPlugin.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public class ConfigurationPropertiesPlugin implements Plugin<Project> {
6565
public static final String CHECK_ADDITIONAL_SPRING_CONFIGURATION_METADATA_TASK_NAME = "checkAdditionalSpringConfigurationMetadata";
6666

6767
/**
68-
* Name of the {@link CheckAdditionalSpringConfigurationMetadata} task.
68+
* Name of the {@link CheckSpringConfigurationMetadata} task.
6969
*/
7070
public static final String CHECK_SPRING_CONFIGURATION_METADATA_TASK_NAME = "checkSpringConfigurationMetadata";
7171

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/*
2+
* Copyright 2025-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.build.test.autoconfigure;
18+
19+
import java.io.File;
20+
import java.io.FileInputStream;
21+
import java.io.IOException;
22+
import java.io.InputStream;
23+
import java.io.UncheckedIOException;
24+
import java.nio.file.Files;
25+
import java.util.ArrayList;
26+
import java.util.Collections;
27+
import java.util.List;
28+
import java.util.Map;
29+
import java.util.TreeMap;
30+
import java.util.function.Consumer;
31+
import java.util.jar.JarFile;
32+
import java.util.stream.Collectors;
33+
import java.util.zip.ZipEntry;
34+
35+
import org.gradle.api.DefaultTask;
36+
import org.gradle.api.file.DirectoryProperty;
37+
import org.gradle.api.file.FileCollection;
38+
import org.gradle.api.file.FileTree;
39+
import org.gradle.api.tasks.Classpath;
40+
import org.gradle.api.tasks.InputFiles;
41+
import org.gradle.api.tasks.OutputDirectory;
42+
import org.gradle.api.tasks.PathSensitive;
43+
import org.gradle.api.tasks.PathSensitivity;
44+
import org.gradle.api.tasks.SkipWhenEmpty;
45+
import org.gradle.api.tasks.TaskAction;
46+
import org.gradle.api.tasks.VerificationException;
47+
import org.gradle.language.base.plugins.LifecycleBasePlugin;
48+
49+
import org.springframework.boot.build.autoconfigure.AutoConfigurationClass;
50+
51+
/**
52+
* Task to check the contents of a project's
53+
* {@code META-INF/spring/*.AutoConfigure*.imports} files.
54+
*
55+
* @author Andy Wilkinson
56+
*/
57+
public abstract class CheckAutoConfigureImports extends DefaultTask {
58+
59+
private FileCollection sourceFiles = getProject().getObjects().fileCollection();
60+
61+
private FileCollection classpath = getProject().getObjects().fileCollection();
62+
63+
public CheckAutoConfigureImports() {
64+
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
65+
setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
66+
}
67+
68+
@InputFiles
69+
@SkipWhenEmpty
70+
@PathSensitive(PathSensitivity.RELATIVE)
71+
public FileTree getSource() {
72+
return this.sourceFiles.getAsFileTree()
73+
.matching((filter) -> filter.include("META-INF/spring/*.AutoConfigure*.imports"));
74+
}
75+
76+
public void setSource(Object source) {
77+
this.sourceFiles = getProject().getObjects().fileCollection().from(source);
78+
}
79+
80+
@Classpath
81+
public FileCollection getClasspath() {
82+
return this.classpath;
83+
}
84+
85+
public void setClasspath(Object classpath) {
86+
this.classpath = getProject().getObjects().fileCollection().from(classpath);
87+
}
88+
89+
@OutputDirectory
90+
public abstract DirectoryProperty getOutputDirectory();
91+
92+
@TaskAction
93+
void execute() {
94+
Map<String, List<String>> allProblems = new TreeMap<>();
95+
for (AutoConfigureImports autoConfigureImports : loadImports()) {
96+
List<String> problems = new ArrayList<>();
97+
if (!find(autoConfigureImports.annotationName)) {
98+
problems.add("Annotation '%s' was not found".formatted(autoConfigureImports.annotationName));
99+
}
100+
for (String imported : autoConfigureImports.imports) {
101+
String importedClassName = imported;
102+
if (importedClassName.startsWith("optional:")) {
103+
importedClassName = importedClassName.substring("optional:".length());
104+
}
105+
boolean found = find(importedClassName, (input) -> {
106+
if (!correctlyAnnotated(input)) {
107+
problems.add("Imported auto-configuration '%s' is not annotated with @AutoConfiguration"
108+
.formatted(imported));
109+
}
110+
});
111+
if (!found) {
112+
problems.add("Imported auto-configuration '%s' was not found".formatted(importedClassName));
113+
}
114+
115+
}
116+
List<String> sortedValues = new ArrayList<>(autoConfigureImports.imports);
117+
Collections.sort(sortedValues, (i1, i2) -> {
118+
boolean imported1 = i1.startsWith("optional:");
119+
boolean imported2 = i2.startsWith("optional:");
120+
int comparison = Boolean.compare(imported1, imported2);
121+
if (comparison != 0) {
122+
return comparison;
123+
}
124+
return i1.compareTo(i2);
125+
});
126+
if (!sortedValues.equals(autoConfigureImports.imports)) {
127+
File sortedOutputFile = getOutputDirectory().file("sorted-" + autoConfigureImports.fileName)
128+
.get()
129+
.getAsFile();
130+
writeString(sortedOutputFile, sortedValues.stream().collect(Collectors.joining(System.lineSeparator()))
131+
+ System.lineSeparator());
132+
problems.add(
133+
"Entries should be required then optional, each sorted alphabetically (expected content written to '%s')"
134+
.formatted(sortedOutputFile.getAbsolutePath()));
135+
}
136+
if (!problems.isEmpty()) {
137+
allProblems.computeIfAbsent(autoConfigureImports.fileName, (unused) -> new ArrayList<>())
138+
.addAll(problems);
139+
}
140+
}
141+
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
142+
writeReport(allProblems, outputFile);
143+
if (!allProblems.isEmpty()) {
144+
throw new VerificationException(
145+
"AutoConfigure….imports checks failed. See '%s' for details".formatted(outputFile));
146+
}
147+
}
148+
149+
private List<AutoConfigureImports> loadImports() {
150+
return getSource().getFiles().stream().map((file) -> {
151+
String fileName = file.getName();
152+
String annotationName = fileName.substring(0, fileName.length() - ".imports".length());
153+
return new AutoConfigureImports(annotationName, loadImports(file), fileName);
154+
}).toList();
155+
}
156+
157+
private List<String> loadImports(File importsFile) {
158+
try {
159+
return Files.readAllLines(importsFile.toPath());
160+
}
161+
catch (IOException ex) {
162+
throw new UncheckedIOException(ex);
163+
}
164+
}
165+
166+
private boolean find(String className) {
167+
return find(className, (input) -> {
168+
});
169+
}
170+
171+
private boolean find(String className, Consumer<InputStream> handler) {
172+
for (File root : this.classpath.getFiles()) {
173+
String classFilePath = className.replace(".", "/") + ".class";
174+
if (root.isDirectory()) {
175+
File classFile = new File(root, classFilePath);
176+
if (classFile.isFile()) {
177+
try (InputStream input = new FileInputStream(classFile)) {
178+
handler.accept(input);
179+
}
180+
catch (IOException ex) {
181+
throw new UncheckedIOException(ex);
182+
}
183+
return true;
184+
}
185+
}
186+
else {
187+
try (JarFile jar = new JarFile(root)) {
188+
ZipEntry entry = jar.getEntry(classFilePath);
189+
if (entry != null) {
190+
try (InputStream input = jar.getInputStream(entry)) {
191+
handler.accept(input);
192+
}
193+
return true;
194+
}
195+
}
196+
catch (IOException ex) {
197+
throw new UncheckedIOException(ex);
198+
}
199+
}
200+
}
201+
return false;
202+
}
203+
204+
private boolean correctlyAnnotated(InputStream classFile) {
205+
return AutoConfigurationClass.of(classFile) != null;
206+
}
207+
208+
private void writeReport(Map<String, List<String>> allProblems, File outputFile) {
209+
outputFile.getParentFile().mkdirs();
210+
StringBuilder report = new StringBuilder();
211+
if (!allProblems.isEmpty()) {
212+
allProblems.forEach((fileName, problems) -> {
213+
report.append("Found problems in '%s':%n".formatted(fileName));
214+
problems.forEach((problem) -> report.append(" - %s%n".formatted(problem)));
215+
});
216+
}
217+
writeString(outputFile, report.toString());
218+
}
219+
220+
private void writeString(File file, String content) {
221+
try {
222+
Files.writeString(file.toPath(), content);
223+
}
224+
catch (IOException ex) {
225+
throw new UncheckedIOException(ex);
226+
}
227+
}
228+
229+
record AutoConfigureImports(String annotationName, List<String> imports, String fileName) {
230+
231+
}
232+
233+
}

0 commit comments

Comments
 (0)