Polish "Add module to support testing of generated code"

See gh-28120

Co-authored-by: Andy Wilkinson <wilkinsona@vmware.com>
This commit is contained in:
Stephane Nicoll 2022-03-02 07:55:06 +01:00
parent 653dc5951d
commit 7255a8b48e
16 changed files with 146 additions and 47 deletions

View File

@ -1,5 +1,3 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
description = "Spring Core Test"
dependencies {

View File

@ -25,6 +25,7 @@ import org.springframework.aot.test.generator.file.ResourceFile;
import org.springframework.aot.test.generator.file.ResourceFiles;
import org.springframework.aot.test.generator.file.SourceFile;
import org.springframework.aot.test.generator.file.SourceFiles;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
@ -42,6 +43,7 @@ public class Compiled {
private final ResourceFiles resourceFiles;
@Nullable
private List<Class<?>> compiledClasses;
@ -107,8 +109,7 @@ public class Compiled {
* @throws IllegalStateException if no instance can be found or instantiated
*/
public <T> T getInstance(Class<T> type) {
List<Class<?>> matching = getAllCompiledClasses().stream().filter(
candidate -> type.isAssignableFrom(candidate)).toList();
List<Class<?>> matching = getAllCompiledClasses().stream().filter(type::isAssignableFrom).toList();
Assert.state(!matching.isEmpty(), () -> "No instance found of type " + type.getName());
Assert.state(matching.size() == 1, () -> "Multiple instances found of type " + type.getName());
return newInstance(matching.get(0));

View File

@ -17,7 +17,6 @@
package org.springframework.aot.test.generator.compile;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
@ -32,7 +31,7 @@ import javax.tools.SimpleJavaFileObject;
*/
class DynamicClassFileObject extends SimpleJavaFileObject {
private volatile byte[] bytes;
private volatile byte[] bytes = new byte[0];
DynamicClassFileObject(String className) {
@ -42,7 +41,7 @@ class DynamicClassFileObject extends SimpleJavaFileObject {
@Override
public OutputStream openOutputStream() throws IOException {
public OutputStream openOutputStream() {
return new JavaClassOutputStream();
}
@ -54,7 +53,7 @@ class DynamicClassFileObject extends SimpleJavaFileObject {
class JavaClassOutputStream extends ByteArrayOutputStream {
@Override
public void close() throws IOException {
public void close() {
DynamicClassFileObject.this.bytes = toByteArray();
}

View File

@ -23,6 +23,7 @@ import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
@ -35,17 +36,18 @@ import org.springframework.aot.test.generator.file.ResourceFile;
import org.springframework.aot.test.generator.file.ResourceFiles;
import org.springframework.aot.test.generator.file.SourceFile;
import org.springframework.aot.test.generator.file.SourceFiles;
import org.springframework.lang.Nullable;
/**
* {@link ClassLoader} used to expose dynamically generated content.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @since 6.0
*/
public class DynamicClassLoader extends ClassLoader {
private static final Logger logger = System.getLogger(
DynamicClassLoader.class.getName());
private static final Logger logger = System.getLogger(DynamicClassLoader.class.getName());
private final SourceFiles sourceFiles;
@ -54,10 +56,13 @@ public class DynamicClassLoader extends ClassLoader {
private final Map<String, DynamicClassFileObject> classFiles;
private final ClassLoader sourceLoader;
public DynamicClassLoader(ClassLoader parent, SourceFiles sourceFiles,
public DynamicClassLoader(ClassLoader sourceLoader, SourceFiles sourceFiles,
ResourceFiles resourceFiles, Map<String, DynamicClassFileObject> classFiles) {
super(parent);
super(sourceLoader.getParent());
this.sourceLoader = sourceLoader;
this.sourceFiles = sourceFiles;
this.resourceFiles = resourceFiles;
this.classFiles = classFiles;
@ -70,7 +75,22 @@ public class DynamicClassLoader extends ClassLoader {
if (classFile != null) {
return defineClass(name, classFile);
}
return super.findClass(name);
try {
Class<?> fromSourceLoader = this.sourceLoader.loadClass(name);
if (Modifier.isPublic(fromSourceLoader.getModifiers())) {
return fromSourceLoader;
}
}
catch (Exception ex) {
// Continue
}
try (InputStream classStream = this.sourceLoader.getResourceAsStream(name.replace(".", "/") + ".class")) {
byte[] bytes = classStream.readAllBytes();
return defineClass(name, bytes, 0, bytes.length, null);
}
catch (IOException ex) {
throw new ClassNotFoundException(name);
}
}
private Class<?> defineClass(String name, DynamicClassFileObject classFile) {
@ -101,6 +121,7 @@ public class DynamicClassLoader extends ClassLoader {
}
@Override
@Nullable
protected URL findResource(String name) {
ResourceFile file = this.resourceFiles.get(name);
if (file != null) {
@ -118,10 +139,11 @@ public class DynamicClassLoader extends ClassLoader {
private static class SingletonEnumeration<E> implements Enumeration<E> {
@Nullable
private E element;
SingletonEnumeration(E element) {
SingletonEnumeration(@Nullable E element) {
this.element = element;
}
@ -132,6 +154,7 @@ public class DynamicClassLoader extends ClassLoader {
}
@Override
@Nullable
public E nextElement() {
E next = this.element;
this.element = null;

View File

@ -42,7 +42,6 @@ class DynamicJavaFileManager extends ForwardingJavaFileManager<JavaFileManager>
new LinkedHashMap<>());
DynamicJavaFileManager(JavaFileManager fileManager, ClassLoader classLoader) {
super(fileManager);
this.classLoader = classLoader;

View File

@ -16,7 +16,6 @@
package org.springframework.aot.test.generator.compile;
import java.io.IOException;
import java.net.URI;
import javax.tools.JavaFileObject;
@ -43,7 +42,7 @@ class DynamicJavaFileObject extends SimpleJavaFileObject {
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return this.sourceFile.getContent();
}

View File

@ -33,6 +33,7 @@ import org.springframework.aot.test.generator.file.ResourceFiles;
import org.springframework.aot.test.generator.file.SourceFile;
import org.springframework.aot.test.generator.file.SourceFiles;
import org.springframework.aot.test.generator.file.WritableContent;
import org.springframework.lang.Nullable;
/**
* Utility that can be used to dynamically compile and test Java source code.
@ -43,6 +44,7 @@ import org.springframework.aot.test.generator.file.WritableContent;
*/
public final class TestCompiler {
@Nullable
private final ClassLoader classLoader;
private final JavaCompiler compiler;
@ -52,7 +54,7 @@ public final class TestCompiler {
private final ResourceFiles resourceFiles;
private TestCompiler(ClassLoader classLoader, JavaCompiler compiler,
private TestCompiler(@Nullable ClassLoader classLoader, JavaCompiler compiler,
SourceFiles sourceFiles, ResourceFiles resourceFiles) {
this.classLoader = classLoader;
this.compiler = compiler;
@ -186,14 +188,14 @@ public final class TestCompiler {
}
private DynamicClassLoader compile() {
ClassLoader classLoader = (this.classLoader != null) ? this.classLoader
ClassLoader classLoaderToUse = (this.classLoader != null) ? this.classLoader
: Thread.currentThread().getContextClassLoader();
List<DynamicJavaFileObject> compilationUnits = this.sourceFiles.stream().map(
DynamicJavaFileObject::new).toList();
StandardJavaFileManager standardFileManager = this.compiler.getStandardFileManager(
null, null, null);
DynamicJavaFileManager fileManager = new DynamicJavaFileManager(
standardFileManager, classLoader);
standardFileManager, classLoaderToUse);
if (!this.sourceFiles.isEmpty()) {
Errors errors = new Errors();
CompilationTask task = this.compiler.getTask(null, fileManager, errors, null,
@ -203,7 +205,7 @@ public final class TestCompiler {
throw new CompilationException("Unable to compile source" + errors);
}
}
return new DynamicClassLoader(this.classLoader, this.sourceFiles,
return new DynamicClassLoader(classLoaderToUse, this.sourceFiles,
this.resourceFiles, fileManager.getClassFiles());
}
@ -223,8 +225,8 @@ public final class TestCompiler {
this.message.append(" ");
this.message.append(diagnostic.getSource().getName());
this.message.append(" ");
this.message.append(
diagnostic.getLineNumber() + ":" + diagnostic.getColumnNumber());
this.message.append(diagnostic.getLineNumber()).append(":")
.append(diagnostic.getColumnNumber());
}
}

View File

@ -0,0 +1,9 @@
/**
* Support classes for compiling and testing generated code.
*/
@NonNullApi
@NonNullFields
package org.springframework.aot.test.generator.compile;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@ -19,7 +19,7 @@ package org.springframework.aot.test.generator.file;
import java.io.IOException;
import java.util.Objects;
import org.assertj.core.util.Strings;
import org.springframework.util.Assert;
/**
* Abstract base class for dynamically generated files.
@ -29,7 +29,7 @@ import org.assertj.core.util.Strings;
* @see SourceFile
* @see ResourceFile
*/
public abstract sealed class DynamicFile permits SourceFile,ResourceFile {
public abstract sealed class DynamicFile permits SourceFile, ResourceFile {
private final String path;
@ -38,21 +38,14 @@ public abstract sealed class DynamicFile permits SourceFile,ResourceFile {
protected DynamicFile(String path, String content) {
if (Strings.isNullOrEmpty(content)) {
throw new IllegalArgumentException("'path' must not to be empty");
}
if (Strings.isNullOrEmpty(content)) {
throw new IllegalArgumentException("'content' must not to be empty");
}
Assert.hasText(path, "Path must not be empty");
Assert.hasText(content, "Content must not be empty");
this.path = path;
this.content = content;
}
protected static String toString(WritableContent writableContent) {
if (writableContent == null) {
throw new IllegalArgumentException("'writableContent' must not to be empty");
}
try {
StringBuilder stringBuilder = new StringBuilder();
writableContent.writeTo(stringBuilder);

View File

@ -18,6 +18,8 @@ package org.springframework.aot.test.generator.file;
import org.assertj.core.api.AbstractAssert;
import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat;
/**
@ -42,7 +44,7 @@ public class DynamicFileAssert<A extends DynamicFileAssert<A, F>, F extends Dyna
return this.myself;
}
public A isEqualTo(Object expected) {
public A isEqualTo(@Nullable Object expected) {
if (expected instanceof DynamicFile) {
return super.isEqualTo(expected);
}

View File

@ -27,6 +27,8 @@ import com.thoughtworks.qdox.model.JavaType;
import org.assertj.core.error.BasicErrorMessageFactory;
import org.assertj.core.internal.Failures;
import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat;
/**
@ -43,11 +45,11 @@ public class SourceFileAssert extends DynamicFileAssert<SourceFileAssert, Source
}
public SourceFileAssert implementsInterface(Class<?> type) {
return implementsInterface((type != null) ? type.getName() : (String) null);
public SourceFileAssert implementsInterface(@Nullable Class<?> type) {
return implementsInterface((type != null ? type.getName() : null));
}
public SourceFileAssert implementsInterface(String name) {
public SourceFileAssert implementsInterface(@Nullable String name) {
JavaClass javaClass = getJavaClass();
assertThat(javaClass.getImplements()).as("implements").map(
JavaType::getFullyQualifiedName).contains(name);

View File

@ -0,0 +1,11 @@
/**
* Support classes for running assertions on generated files.
*
* @author Stephane Nicoll
*/
@NonNullApi
@NonNullFields
package org.springframework.aot.test.generator.file;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@ -0,0 +1,26 @@
/*
* Copyright 2002-2022 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 com.example;
class PackagePrivate {
String perform() {
return "Hello from PackagePrivate";
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2002-2022 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 com.example;
public interface PublicInterface {
String perform();
}

View File

@ -18,6 +18,7 @@ package org.springframework.aot.test.generator.compile;
import java.util.function.Supplier;
import com.example.PublicInterface;
import org.junit.jupiter.api.Test;
import org.springframework.aot.test.generator.file.ResourceFile;
@ -33,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
* Tests for {@link TestCompiler}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class TestCompilerTests {
@ -106,7 +108,7 @@ class TestCompilerTests {
assertThatExceptionOfType(CompilationException.class).isThrownBy(
() -> TestCompiler.forSystem().withSources(
SourceFile.of(HELLO_BAD)).compile(compiled -> {
}));
}));
}
@Test
@ -167,6 +169,24 @@ class TestCompilerTests {
});
}
@Test
void compiledCodeCanAccessExistingPackagePrivateClass() {
SourceFiles sourceFiles = SourceFiles.of(SourceFile.of("""
package com.example;
public class Test implements PublicInterface {
public String perform() {
return new PackagePrivate().perform();
}
}
"""));
TestCompiler.forSystem().compile(sourceFiles, compiled -> assertThat(
compiled.getInstance(PublicInterface.class, "com.example.Test").perform())
.isEqualTo("Hello from PackagePrivate"));
}
private void assertSuppliesHelloWorld(Compiled compiled) {
assertThat(compiled.getInstance(Supplier.class).get()).isEqualTo("Hello World!");
}

View File

@ -22,7 +22,6 @@ import com.thoughtworks.qdox.model.JavaSource;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
@ -42,13 +41,6 @@ class SourceFileTests {
}
""";
@Test
void ofWhenContentIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(
() -> SourceFile.of((WritableContent) null)).withMessage(
"'writableContent' must not to be empty");
}
@Test
void ofWhenContentIsEmptyThrowsException() {
assertThatIllegalStateException().isThrownBy(() -> SourceFile.of("")).withMessage(