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:
parent
653dc5951d
commit
7255a8b48e
|
@ -1,5 +1,3 @@
|
|||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
|
||||
description = "Spring Core Test"
|
||||
|
||||
dependencies {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -42,7 +42,6 @@ class DynamicJavaFileManager extends ForwardingJavaFileManager<JavaFileManager>
|
|||
new LinkedHashMap<>());
|
||||
|
||||
|
||||
|
||||
DynamicJavaFileManager(JavaFileManager fileManager, ClassLoader classLoader) {
|
||||
super(fileManager);
|
||||
this.classLoader = classLoader;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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!");
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue