Browse Source

Apply checks for manual configuration metadata

This commit adds a 'org.springframework.boot.configuration-metadata'
plugin to be used for projects that only define manual metadata. Such
project do not need the annotation processor, but do not to check that
the structure of the metadata content matches the same rules.

Closes gh-47984
pull/47980/head
Stéphane Nicoll 1 month ago
parent
commit
52951ed8f4
  1. 4
      buildSrc/build.gradle
  2. 107
      buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckAdditionalSpringConfigurationMetadata.java
  3. 77
      buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckManualSpringConfigurationMetadata.java
  4. 98
      buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckSpringConfigurationMetadata.java
  5. 76
      buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationMetadataPlugin.java
  6. 251
      buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesAnalyzer.java
  7. 185
      buildSrc/src/test/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesAnalyzerTests.java

4
buildSrc/build.gradle

@ -112,6 +112,10 @@ gradlePlugin { @@ -112,6 +112,10 @@ gradlePlugin {
id = "org.springframework.boot.bom"
implementationClass = "org.springframework.boot.build.bom.BomPlugin"
}
configurationMetadataPlugin {
id = "org.springframework.boot.configuration-metadata"
implementationClass = "org.springframework.boot.build.context.properties.ConfigurationMetadataPlugin"
}
configurationPropertiesPlugin {
id = "org.springframework.boot.configuration-properties"
implementationClass = "org.springframework.boot.build.context.properties.ConfigurationPropertiesPlugin"

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

@ -18,19 +18,7 @@ package org.springframework.boot.build.context.properties; @@ -18,19 +18,7 @@ package org.springframework.boot.build.context.properties;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.gradle.api.file.FileTree;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.tasks.InputFiles;
@ -41,6 +29,8 @@ import org.gradle.api.tasks.SourceTask; @@ -41,6 +29,8 @@ import org.gradle.api.tasks.SourceTask;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.VerificationException;
import org.springframework.boot.build.context.properties.ConfigurationPropertiesAnalyzer.Report;
/**
* {@link SourceTask} that checks additional Spring configuration metadata files.
*
@ -65,99 +55,16 @@ public abstract class CheckAdditionalSpringConfigurationMetadata extends SourceT @@ -65,99 +55,16 @@ public abstract class CheckAdditionalSpringConfigurationMetadata extends SourceT
}
@TaskAction
void check() throws JsonParseException, IOException {
Report report = createReport();
void check() throws IOException {
ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(getSource().getFiles());
Report report = new Report(this.projectDir);
analyzer.analyzeSort(report);
File reportFile = getReportLocation().get().getAsFile();
Files.write(reportFile.toPath(), report, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
report.write(reportFile);
if (report.hasProblems()) {
throw new VerificationException(
"Problems found in additional Spring configuration metadata. See " + reportFile + " for details.");
}
}
@SuppressWarnings("unchecked")
private Report createReport() throws IOException, JsonParseException, JsonMappingException {
ObjectMapper objectMapper = new ObjectMapper();
Report report = new Report();
for (File file : getSource().getFiles()) {
Analysis analysis = report.analysis(this.projectDir.toPath().relativize(file.toPath()));
Map<String, Object> json = objectMapper.readValue(file, Map.class);
check("groups", json, analysis);
check("properties", json, analysis);
check("hints", json, analysis);
}
return report;
}
@SuppressWarnings("unchecked")
private void check(String key, Map<String, Object> json, Analysis analysis) {
List<Map<String, Object>> groups = (List<Map<String, Object>>) json.getOrDefault(key, Collections.emptyList());
List<String> names = groups.stream().map((group) -> (String) group.get("name")).toList();
List<String> sortedNames = sortedCopy(names);
for (int i = 0; i < names.size(); i++) {
String actual = names.get(i);
String expected = sortedNames.get(i);
if (!actual.equals(expected)) {
analysis.problems.add("Wrong order at $." + key + "[" + i + "].name - expected '" + expected
+ "' but found '" + actual + "'");
}
}
}
private List<String> sortedCopy(Collection<String> original) {
List<String> copy = new ArrayList<>(original);
Collections.sort(copy);
return copy;
}
private static final class Report implements Iterable<String> {
private final List<Analysis> analyses = new ArrayList<>();
private Analysis analysis(Path path) {
Analysis analysis = new Analysis(path);
this.analyses.add(analysis);
return analysis;
}
private boolean hasProblems() {
for (Analysis analysis : this.analyses) {
if (!analysis.problems.isEmpty()) {
return true;
}
}
return false;
}
@Override
public Iterator<String> iterator() {
List<String> lines = new ArrayList<>();
for (Analysis analysis : this.analyses) {
lines.add(analysis.source.toString());
lines.add("");
if (analysis.problems.isEmpty()) {
lines.add("No problems found.");
}
else {
lines.addAll(analysis.problems);
}
lines.add("");
}
return lines.iterator();
}
}
private static final class Analysis {
private final List<String> problems = new ArrayList<>();
private final Path source;
private Analysis(Path source) {
this.source = source;
}
}
}

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

@ -0,0 +1,77 @@ @@ -0,0 +1,77 @@
/*
* Copyright 2012-present the original author or authors.
*
* 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
*
* https://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 org.springframework.boot.build.context.properties;
import java.io.File;
import java.io.IOException;
import java.util.List;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.SourceTask;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.VerificationException;
import org.springframework.boot.build.context.properties.ConfigurationPropertiesAnalyzer.Report;
/**
* {@link SourceTask} that checks manual Spring configuration metadata files.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
*/
public abstract class CheckManualSpringConfigurationMetadata extends DefaultTask {
private final File projectDir;
public CheckManualSpringConfigurationMetadata() {
this.projectDir = getProject().getProjectDir();
}
@OutputFile
public abstract RegularFileProperty getReportLocation();
@InputFile
@PathSensitive(PathSensitivity.RELATIVE)
public abstract Property<File> getMetadataLocation();
@Input
public abstract ListProperty<String> getExclusions();
@TaskAction
void check() throws IOException {
ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(
List.of(getMetadataLocation().get()));
Report report = new Report(this.projectDir);
analyzer.analyzeSort(report);
analyzer.analyzePropertyDescription(report, getExclusions().get());
File reportFile = getReportLocation().get().getAsFile();
report.write(reportFile);
if (report.hasProblems()) {
throw new VerificationException(
"Problems found in manual Spring configuration metadata. See " + reportFile + " for details.");
}
}
}

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

@ -18,17 +18,8 @@ package org.springframework.boot.build.context.properties; @@ -18,17 +18,8 @@ package org.springframework.boot.build.context.properties;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.ListProperty;
@ -41,6 +32,8 @@ import org.gradle.api.tasks.SourceTask; @@ -41,6 +32,8 @@ import org.gradle.api.tasks.SourceTask;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.VerificationException;
import org.springframework.boot.build.context.properties.ConfigurationPropertiesAnalyzer.Report;
/**
* {@link SourceTask} that checks {@code spring-configuration-metadata.json} files.
*
@ -48,10 +41,10 @@ import org.gradle.api.tasks.VerificationException; @@ -48,10 +41,10 @@ import org.gradle.api.tasks.VerificationException;
*/
public abstract class CheckSpringConfigurationMetadata extends DefaultTask {
private final Path projectRoot;
private final File projectRoot;
public CheckSpringConfigurationMetadata() {
this.projectRoot = getProject().getProjectDir().toPath();
this.projectRoot = getProject().getProjectDir();
}
@OutputFile
@ -65,87 +58,18 @@ public abstract class CheckSpringConfigurationMetadata extends DefaultTask { @@ -65,87 +58,18 @@ public abstract class CheckSpringConfigurationMetadata extends DefaultTask {
public abstract ListProperty<String> getExclusions();
@TaskAction
void check() throws JsonParseException, IOException {
Report report = createReport();
void check() throws IOException {
Report report = new Report(this.projectRoot);
ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(
List.of(getMetadataLocation().get().getAsFile()));
analyzer.analyzePropertyDescription(report, getExclusions().get());
File reportFile = getReportLocation().get().getAsFile();
Files.write(reportFile.toPath(), report, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
report.write(reportFile);
if (report.hasProblems()) {
throw new VerificationException(
"Problems found in Spring configuration metadata. See " + reportFile + " for details.");
}
}
@SuppressWarnings("unchecked")
private Report createReport() throws IOException, JsonParseException, JsonMappingException {
ObjectMapper objectMapper = new ObjectMapper();
File file = getMetadataLocation().get().getAsFile();
Report report = new Report(this.projectRoot.relativize(file.toPath()));
Map<String, Object> json = objectMapper.readValue(file, Map.class);
List<Map<String, Object>> properties = (List<Map<String, Object>>) json.get("properties");
for (Map<String, Object> property : properties) {
String name = (String) property.get("name");
if (!isDeprecated(property) && !isDescribed(property) && !isExcluded(name)) {
report.propertiesWithNoDescription.add(name);
}
}
return report;
}
private boolean isExcluded(String propertyName) {
for (String exclusion : getExclusions().get()) {
if (propertyName.equals(exclusion)) {
return true;
}
if (exclusion.endsWith(".*")) {
if (propertyName.startsWith(exclusion.substring(0, exclusion.length() - 2))) {
return true;
}
}
}
return false;
}
@SuppressWarnings("unchecked")
private boolean isDeprecated(Map<String, Object> property) {
return (Map<String, Object>) property.get("deprecation") != null;
}
private boolean isDescribed(Map<String, Object> property) {
return property.get("description") != null;
}
private static final class Report implements Iterable<String> {
private final List<String> propertiesWithNoDescription = new ArrayList<>();
private final Path source;
private Report(Path source) {
this.source = source;
}
private boolean hasProblems() {
return !this.propertiesWithNoDescription.isEmpty();
}
@Override
public Iterator<String> iterator() {
List<String> lines = new ArrayList<>();
lines.add(this.source.toString());
lines.add("");
if (this.propertiesWithNoDescription.isEmpty()) {
lines.add("No problems found.");
}
else {
lines.add("The following properties have no description:");
lines.add("");
lines.addAll(this.propertiesWithNoDescription.stream().map((line) -> "\t" + line).toList());
}
lines.add("");
return lines.iterator();
}
}
}

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

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
/*
* Copyright 2012-present the original author or authors.
*
* 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
*
* https://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 org.springframework.boot.build.context.properties;
import java.io.File;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.language.base.plugins.LifecycleBasePlugin;
import org.gradle.language.jvm.tasks.ProcessResources;
/**
* {@link Plugin} for projects that <em>only</em> define manual configuration metadata.
* When applied, the plugin registers a {@link CheckManualSpringConfigurationMetadata}
* task and configures the {@code check} task to depend upon it.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
*/
public class ConfigurationMetadataPlugin implements Plugin<Project> {
/**
* Name of the {@link CheckAdditionalSpringConfigurationMetadata} task.
*/
public static final String CHECK_MANUAL_SPRING_CONFIGURATION_METADATA_TASK_NAME = "checkManualSpringConfigurationMetadata";
@Override
public void apply(Project project) {
project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> registerCheckAdditionalMetadataTask(project));
}
private void registerCheckAdditionalMetadataTask(Project project) {
TaskProvider<CheckManualSpringConfigurationMetadata> checkConfigurationMetadata = project.getTasks()
.register(CHECK_MANUAL_SPRING_CONFIGURATION_METADATA_TASK_NAME,
CheckManualSpringConfigurationMetadata.class);
checkConfigurationMetadata.configure((check) -> {
SourceSet mainSourceSet = project.getExtensions()
.getByType(JavaPluginExtension.class)
.getSourceSets()
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
Provider<File> manualMetadataLocation = project.getTasks()
.named(mainSourceSet.getProcessResourcesTaskName(), ProcessResources.class)
.map((processResources) -> new File(processResources.getDestinationDir(),
"META-INF/spring-configuration-metadata.json"));
check.getMetadataLocation().set(manualMetadataLocation);
check.getReportLocation()
.set(project.getLayout()
.getBuildDirectory()
.file("reports/manual-spring-configuration-metadata/check.txt"));
});
project.getTasks()
.named(LifecycleBasePlugin.CHECK_TASK_NAME)
.configure((check) -> check.dependsOn(checkConfigurationMetadata));
}
}

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

@ -0,0 +1,251 @@ @@ -0,0 +1,251 @@
/*
* Copyright 2012-present the original author or authors.
*
* 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
*
* https://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 org.springframework.boot.build.context.properties;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.function.SingletonSupplier;
/**
* Check configuration metadata for inconsistencies. The available checks are:
* <ul>
* <li>Metadata element should be sorted alphabetically: {@link #analyzeSort(Report)}</li>
* <li>Property must have a description:
* {@link #analyzePropertyDescription(Report, List)}</li>
* </ul>
*
* @author Stephane Nicoll
*/
class ConfigurationPropertiesAnalyzer {
private final Collection<File> sources;
private final SingletonSupplier<ObjectMapper> objectMapperSupplier;
ConfigurationPropertiesAnalyzer(Collection<File> sources) {
if (sources.isEmpty()) {
throw new IllegalArgumentException("At least one source should be provided");
}
this.sources = sources;
this.objectMapperSupplier = SingletonSupplier.of(ObjectMapper::new);
}
void analyzeSort(Report report) throws IOException {
for (File source : this.sources) {
report.registerAnalysis(source, analyzeSort(source));
}
}
private Analysis analyzeSort(File source) throws IOException {
Map<String, Object> json = readJsonContent(source);
Analysis analysis = new Analysis("Metadata element order:");
analyzeMetadataElementsSort("groups", json, analysis);
analyzeMetadataElementsSort("properties", json, analysis);
analyzeMetadataElementsSort("hints", json, analysis);
return analysis;
}
@SuppressWarnings("unchecked")
private void analyzeMetadataElementsSort(String key, Map<String, Object> json, Analysis analysis) {
List<Map<String, Object>> groups = (List<Map<String, Object>>) json.getOrDefault(key, Collections.emptyList());
List<String> names = groups.stream().map((group) -> (String) group.get("name")).toList();
List<String> sortedNames = names.stream().sorted().toList();
for (int i = 0; i < names.size(); i++) {
String actual = names.get(i);
String expected = sortedNames.get(i);
if (!actual.equals(expected)) {
analysis.addItem("Wrong order at $." + key + "[" + i + "].name - expected '" + expected
+ "' but found '" + actual + "'");
}
}
}
void analyzePropertyDescription(Report report, List<String> exclusions) throws IOException {
for (File source : this.sources) {
report.registerAnalysis(source, analyzePropertyDescription(source, exclusions));
}
}
@SuppressWarnings("unchecked")
private Analysis analyzePropertyDescription(File source, List<String> exclusions) throws IOException {
Map<String, Object> json = readJsonContent(source);
Analysis analysis = new Analysis("The following properties have no description:");
List<Map<String, Object>> properties = (List<Map<String, Object>>) json.get("properties");
for (Map<String, Object> property : properties) {
String name = (String) property.get("name");
if (!isDeprecated(property) && !isDescribed(property) && !isExcluded(exclusions, name)) {
analysis.addItem(name);
}
}
return analysis;
}
private boolean isExcluded(List<String> exclusions, String propertyName) {
for (String exclusion : exclusions) {
if (propertyName.equals(exclusion)) {
return true;
}
if (exclusion.endsWith(".*")) {
if (propertyName.startsWith(exclusion.substring(0, exclusion.length() - 2))) {
return true;
}
}
}
return false;
}
private boolean isDeprecated(Map<String, Object> property) {
return property.get("deprecation") != null;
}
private boolean isDescribed(Map<String, Object> property) {
return property.get("description") != null;
}
private Map<String, Object> readJsonContent(File source) throws IOException {
return this.objectMapperSupplier.obtain().readValue(source, new TypeReference<Map<String, Object>>() {
});
}
private static <T> void writeAll(PrintWriter writer, Iterable<T> elements, Consumer<T> itemWriter) {
Iterator<T> it = elements.iterator();
while (it.hasNext()) {
itemWriter.accept(it.next());
if (it.hasNext()) {
writer.println();
}
}
}
static class Report {
private final File baseDirectory;
private final MultiValueMap<File, Analysis> analyses = new LinkedMultiValueMap<>();
Report(File baseDirectory) {
this.baseDirectory = baseDirectory;
}
void registerAnalysis(File path, Analysis analysis) {
this.analyses.add(path, analysis);
}
boolean hasProblems() {
return this.analyses.values()
.stream()
.anyMatch((candidates) -> candidates.stream().anyMatch(Analysis::hasProblems));
}
List<Analysis> getAnalyses(File source) {
return this.analyses.getOrDefault(source, Collections.emptyList());
}
/**
* Write this report to the given {@code file}.
* @param file the file to write the report to
*/
void write(File file) throws IOException {
Files.writeString(file.toPath(), createContent(), StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
}
private String createContent() {
if (this.analyses.isEmpty()) {
return "No problems found.";
}
StringWriter out = new StringWriter();
try (PrintWriter writer = new PrintWriter(out)) {
writeAll(writer, this.analyses.entrySet(), (entry) -> {
writer.println(this.baseDirectory.toPath().relativize(entry.getKey().toPath()));
boolean hasProblems = entry.getValue().stream().anyMatch(Analysis::hasProblems);
if (hasProblems) {
writeAll(writer, entry.getValue(), (analysis) -> analysis.createDetails(writer));
}
else {
writer.println("No problems found.");
}
});
}
return out.toString();
}
}
static class Analysis {
private final String header;
private final List<String> items;
Analysis(String header) {
this.header = header;
this.items = new ArrayList<>();
}
void addItem(String item) {
this.items.add(item);
}
boolean hasProblems() {
return !this.items.isEmpty();
}
List<String> getItems() {
return this.items;
}
void createDetails(PrintWriter writer) {
writer.println(this.header);
if (this.items.isEmpty()) {
writer.println("No problems found.");
}
else {
for (String item : this.items) {
writer.println("\t- " + item);
}
}
}
@Override
public String toString() {
StringWriter out = new StringWriter();
PrintWriter writer = new PrintWriter(out);
createDetails(writer);
return out.toString();
}
}
}

185
buildSrc/src/test/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesAnalyzerTests.java

@ -0,0 +1,185 @@ @@ -0,0 +1,185 @@
/*
* Copyright 2012-present the original author or authors.
*
* 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
*
* https://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 org.springframework.boot.build.context.properties;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.build.context.properties.ConfigurationPropertiesAnalyzer.Analysis;
import org.springframework.boot.build.context.properties.ConfigurationPropertiesAnalyzer.Report;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link ConfigurationPropertiesAnalyzer}.
*
* @author Stephane Nicoll
*/
class ConfigurationPropertiesAnalyzerTests {
@Test
void createAnalyzerWithNoSource() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new ConfigurationPropertiesAnalyzer(Collections.emptyList()))
.withMessage("At least one source should be provided");
}
@Test
void analyzeSortWithAlphabeticalOrder(@TempDir File tempDir) throws IOException {
File metadata = new File(tempDir, "metadata.json");
Files.writeString(metadata.toPath(), """
{ "properties": [
{ "name": "abc"}, {"name": "def"}, {"name": "xyz"}
]
}""");
Report report = new Report(tempDir);
ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(List.of(metadata));
analyzer.analyzeSort(report);
assertThat(report.hasProblems()).isFalse();
assertThat(report.getAnalyses(metadata)).singleElement()
.satisfies(((analysis) -> assertThat(analysis.getItems()).isEmpty()));
}
@Test
void analyzeSortWithViolations(@TempDir File tempDir) throws IOException {
File metadata = new File(tempDir, "metadata.json");
Files.writeString(metadata.toPath(), """
{ "properties": [
{ "name": "def"}, {"name": "abc"}, {"name": "xyz"}
]
}""");
Report report = new Report(tempDir);
ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(List.of(metadata));
analyzer.analyzeSort(report);
assertThat(report.hasProblems()).isTrue();
assertThat(report.getAnalyses(metadata)).singleElement()
.satisfies((analysis) -> assertThat(analysis.getItems()).containsExactly(
"Wrong order at $.properties[0].name - expected 'abc' but found 'def'",
"Wrong order at $.properties[1].name - expected 'def' but found 'abc'"));
}
@Test
void analyzePropertyDescription(@TempDir File tempDir) throws IOException {
File metadata = new File(tempDir, "metadata.json");
Files.writeString(metadata.toPath(), """
{ "properties": [
{ "name": "abc", "description": "This is abc." },
{ "name": "def", "description": "This is def." },
{ "name": "xyz", "description": "This is xyz." }
]
}""");
Report report = new Report(tempDir);
ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(List.of(metadata));
analyzer.analyzePropertyDescription(report, List.of());
assertThat(report.hasProblems()).isFalse();
assertThat(report.getAnalyses(metadata)).singleElement()
.satisfies(((analysis) -> assertThat(analysis.getItems()).isEmpty()));
}
@Test
void analyzePropertyDescriptionWithMissingDescription(@TempDir File tempDir) throws IOException {
File metadata = new File(tempDir, "metadata.json");
Files.writeString(metadata.toPath(), """
{ "properties": [
{ "name": "abc", "description": "This is abc." },
{ "name": "def" },
{ "name": "xyz", "description": "This is xyz." }
]
}""");
Report report = new Report(tempDir);
ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(List.of(metadata));
analyzer.analyzePropertyDescription(report, List.of());
assertThat(report.hasProblems()).isTrue();
assertThat(report.getAnalyses(metadata)).singleElement()
.satisfies(((analysis) -> assertThat(analysis.getItems()).containsExactly("def")));
}
@Test
void writeEmptyReport(@TempDir File tempDir) throws IOException {
assertThat(writeToFile(tempDir, new Report(tempDir))).hasContent("No problems found.");
}
@Test
void writeReportWithNoProblemsFound(@TempDir File tempDir) throws IOException {
Report report = new Report(tempDir);
File first = new File(tempDir, "metadata-1.json");
report.registerAnalysis(first, new Analysis("Check for things:"));
File second = new File(tempDir, "metadata-2.json");
report.registerAnalysis(second, new Analysis("Check for other things:"));
assertThat(writeToFile(tempDir, report)).content().isEqualTo("""
metadata-1.json
No problems found.
metadata-2.json
No problems found.
""");
}
@Test
void writeReportWithOneProblem(@TempDir File tempDir) throws IOException {
Report report = new Report(tempDir);
File metadata = new File(tempDir, "metadata-1.json");
Analysis analysis = new Analysis("Check for things:");
analysis.addItem("Should not be deprecated");
report.registerAnalysis(metadata, analysis);
report.registerAnalysis(metadata, new Analysis("Check for other things:"));
assertThat(writeToFile(tempDir, report)).content().isEqualTo("""
metadata-1.json
Check for things:
- Should not be deprecated
Check for other things:
No problems found.
""");
}
@Test
void writeReportWithSeveralProblems(@TempDir File tempDir) throws IOException {
Report report = new Report(tempDir);
File metadata = new File(tempDir, "metadata-1.json");
Analysis firstAnalysis = new Analysis("Check for things:");
firstAnalysis.addItem("Should not be deprecated");
firstAnalysis.addItem("Should not be public");
report.registerAnalysis(metadata, firstAnalysis);
Analysis secondAnalysis = new Analysis("Check for other things:");
secondAnalysis.addItem("Field 'this' not expected");
report.registerAnalysis(metadata, secondAnalysis);
assertThat(writeToFile(tempDir, report)).content().isEqualTo("""
metadata-1.json
Check for things:
- Should not be deprecated
- Should not be public
Check for other things:
- Field 'this' not expected
""");
}
private File writeToFile(File directory, Report report) throws IOException {
File file = new File(directory, "report.txt");
report.write(file);
return file;
}
}
Loading…
Cancel
Save