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

View File

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

View File

@ -23,6 +23,7 @@ import java.lang.System.Logger;
import java.lang.System.Logger.Level; import java.lang.System.Logger.Level;
import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup; import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.net.URLConnection; 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.ResourceFiles;
import org.springframework.aot.test.generator.file.SourceFile; import org.springframework.aot.test.generator.file.SourceFile;
import org.springframework.aot.test.generator.file.SourceFiles; import org.springframework.aot.test.generator.file.SourceFiles;
import org.springframework.lang.Nullable;
/** /**
* {@link ClassLoader} used to expose dynamically generated content. * {@link ClassLoader} used to expose dynamically generated content.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Andy Wilkinson
* @since 6.0 * @since 6.0
*/ */
public class DynamicClassLoader extends ClassLoader { public class DynamicClassLoader extends ClassLoader {
private static final Logger logger = System.getLogger( private static final Logger logger = System.getLogger(DynamicClassLoader.class.getName());
DynamicClassLoader.class.getName());
private final SourceFiles sourceFiles; private final SourceFiles sourceFiles;
@ -54,10 +56,13 @@ public class DynamicClassLoader extends ClassLoader {
private final Map<String, DynamicClassFileObject> classFiles; 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) { ResourceFiles resourceFiles, Map<String, DynamicClassFileObject> classFiles) {
super(parent); super(sourceLoader.getParent());
this.sourceLoader = sourceLoader;
this.sourceFiles = sourceFiles; this.sourceFiles = sourceFiles;
this.resourceFiles = resourceFiles; this.resourceFiles = resourceFiles;
this.classFiles = classFiles; this.classFiles = classFiles;
@ -70,7 +75,22 @@ public class DynamicClassLoader extends ClassLoader {
if (classFile != null) { if (classFile != null) {
return defineClass(name, classFile); 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) { private Class<?> defineClass(String name, DynamicClassFileObject classFile) {
@ -101,6 +121,7 @@ public class DynamicClassLoader extends ClassLoader {
} }
@Override @Override
@Nullable
protected URL findResource(String name) { protected URL findResource(String name) {
ResourceFile file = this.resourceFiles.get(name); ResourceFile file = this.resourceFiles.get(name);
if (file != null) { if (file != null) {
@ -118,10 +139,11 @@ public class DynamicClassLoader extends ClassLoader {
private static class SingletonEnumeration<E> implements Enumeration<E> { private static class SingletonEnumeration<E> implements Enumeration<E> {
@Nullable
private E element; private E element;
SingletonEnumeration(E element) { SingletonEnumeration(@Nullable E element) {
this.element = element; this.element = element;
} }
@ -132,6 +154,7 @@ public class DynamicClassLoader extends ClassLoader {
} }
@Override @Override
@Nullable
public E nextElement() { public E nextElement() {
E next = this.element; E next = this.element;
this.element = null; this.element = null;

View File

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

View File

@ -16,7 +16,6 @@
package org.springframework.aot.test.generator.compile; package org.springframework.aot.test.generator.compile;
import java.io.IOException;
import java.net.URI; import java.net.URI;
import javax.tools.JavaFileObject; import javax.tools.JavaFileObject;
@ -43,7 +42,7 @@ class DynamicJavaFileObject extends SimpleJavaFileObject {
@Override @Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return this.sourceFile.getContent(); 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.SourceFile;
import org.springframework.aot.test.generator.file.SourceFiles; import org.springframework.aot.test.generator.file.SourceFiles;
import org.springframework.aot.test.generator.file.WritableContent; 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. * 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 { public final class TestCompiler {
@Nullable
private final ClassLoader classLoader; private final ClassLoader classLoader;
private final JavaCompiler compiler; private final JavaCompiler compiler;
@ -52,7 +54,7 @@ public final class TestCompiler {
private final ResourceFiles resourceFiles; private final ResourceFiles resourceFiles;
private TestCompiler(ClassLoader classLoader, JavaCompiler compiler, private TestCompiler(@Nullable ClassLoader classLoader, JavaCompiler compiler,
SourceFiles sourceFiles, ResourceFiles resourceFiles) { SourceFiles sourceFiles, ResourceFiles resourceFiles) {
this.classLoader = classLoader; this.classLoader = classLoader;
this.compiler = compiler; this.compiler = compiler;
@ -186,14 +188,14 @@ public final class TestCompiler {
} }
private DynamicClassLoader compile() { private DynamicClassLoader compile() {
ClassLoader classLoader = (this.classLoader != null) ? this.classLoader ClassLoader classLoaderToUse = (this.classLoader != null) ? this.classLoader
: Thread.currentThread().getContextClassLoader(); : Thread.currentThread().getContextClassLoader();
List<DynamicJavaFileObject> compilationUnits = this.sourceFiles.stream().map( List<DynamicJavaFileObject> compilationUnits = this.sourceFiles.stream().map(
DynamicJavaFileObject::new).toList(); DynamicJavaFileObject::new).toList();
StandardJavaFileManager standardFileManager = this.compiler.getStandardFileManager( StandardJavaFileManager standardFileManager = this.compiler.getStandardFileManager(
null, null, null); null, null, null);
DynamicJavaFileManager fileManager = new DynamicJavaFileManager( DynamicJavaFileManager fileManager = new DynamicJavaFileManager(
standardFileManager, classLoader); standardFileManager, classLoaderToUse);
if (!this.sourceFiles.isEmpty()) { if (!this.sourceFiles.isEmpty()) {
Errors errors = new Errors(); Errors errors = new Errors();
CompilationTask task = this.compiler.getTask(null, fileManager, errors, null, 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); 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()); this.resourceFiles, fileManager.getClassFiles());
} }
@ -223,8 +225,8 @@ public final class TestCompiler {
this.message.append(" "); this.message.append(" ");
this.message.append(diagnostic.getSource().getName()); this.message.append(diagnostic.getSource().getName());
this.message.append(" "); this.message.append(" ");
this.message.append( this.message.append(diagnostic.getLineNumber()).append(":")
diagnostic.getLineNumber() + ":" + diagnostic.getColumnNumber()); .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.io.IOException;
import java.util.Objects; import java.util.Objects;
import org.assertj.core.util.Strings; import org.springframework.util.Assert;
/** /**
* Abstract base class for dynamically generated files. * Abstract base class for dynamically generated files.
@ -38,21 +38,14 @@ public abstract sealed class DynamicFile permits SourceFile,ResourceFile {
protected DynamicFile(String path, String content) { protected DynamicFile(String path, String content) {
if (Strings.isNullOrEmpty(content)) { Assert.hasText(path, "Path must not be empty");
throw new IllegalArgumentException("'path' must not to be empty"); Assert.hasText(content, "Content must not be empty");
}
if (Strings.isNullOrEmpty(content)) {
throw new IllegalArgumentException("'content' must not to be empty");
}
this.path = path; this.path = path;
this.content = content; this.content = content;
} }
protected static String toString(WritableContent writableContent) { protected static String toString(WritableContent writableContent) {
if (writableContent == null) {
throw new IllegalArgumentException("'writableContent' must not to be empty");
}
try { try {
StringBuilder stringBuilder = new StringBuilder(); StringBuilder stringBuilder = new StringBuilder();
writableContent.writeTo(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.assertj.core.api.AbstractAssert;
import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat; 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; return this.myself;
} }
public A isEqualTo(Object expected) { public A isEqualTo(@Nullable Object expected) {
if (expected instanceof DynamicFile) { if (expected instanceof DynamicFile) {
return super.isEqualTo(expected); 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.error.BasicErrorMessageFactory;
import org.assertj.core.internal.Failures; import org.assertj.core.internal.Failures;
import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
/** /**
@ -43,11 +45,11 @@ public class SourceFileAssert extends DynamicFileAssert<SourceFileAssert, Source
} }
public SourceFileAssert implementsInterface(Class<?> type) { public SourceFileAssert implementsInterface(@Nullable Class<?> type) {
return implementsInterface((type != null) ? type.getName() : (String) null); return implementsInterface((type != null ? type.getName() : null));
} }
public SourceFileAssert implementsInterface(String name) { public SourceFileAssert implementsInterface(@Nullable String name) {
JavaClass javaClass = getJavaClass(); JavaClass javaClass = getJavaClass();
assertThat(javaClass.getImplements()).as("implements").map( assertThat(javaClass.getImplements()).as("implements").map(
JavaType::getFullyQualifiedName).contains(name); 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 java.util.function.Supplier;
import com.example.PublicInterface;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.aot.test.generator.file.ResourceFile; import org.springframework.aot.test.generator.file.ResourceFile;
@ -33,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
* Tests for {@link TestCompiler}. * Tests for {@link TestCompiler}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Andy Wilkinson
*/ */
class TestCompilerTests { class TestCompilerTests {
@ -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) { private void assertSuppliesHelloWorld(Compiled compiled) {
assertThat(compiled.getInstance(Supplier.class).get()).isEqualTo("Hello World!"); 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 org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; 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 @Test
void ofWhenContentIsEmptyThrowsException() { void ofWhenContentIsEmptyThrowsException() {
assertThatIllegalStateException().isThrownBy(() -> SourceFile.of("")).withMessage( assertThatIllegalStateException().isThrownBy(() -> SourceFile.of("")).withMessage(