From 7255a8b48e6bedb3e964c44d1a8e00bb6e240d07 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 2 Mar 2022 07:55:06 +0100 Subject: [PATCH] Polish "Add module to support testing of generated code" See gh-28120 Co-authored-by: Andy Wilkinson --- spring-core-test/spring-core-test.gradle | 2 -- .../aot/test/generator/compile/Compiled.java | 5 +-- .../compile/DynamicClassFileObject.java | 7 ++-- .../generator/compile/DynamicClassLoader.java | 35 +++++++++++++++---- .../compile/DynamicJavaFileManager.java | 1 - .../compile/DynamicJavaFileObject.java | 3 +- .../test/generator/compile/TestCompiler.java | 14 ++++---- .../test/generator/compile/package-info.java | 9 +++++ .../aot/test/generator/file/DynamicFile.java | 15 +++----- .../generator/file/DynamicFileAssert.java | 4 ++- .../test/generator/file/SourceFileAssert.java | 8 +++-- .../aot/test/generator/file/package-info.java | 11 ++++++ .../test/java/com/example/PackagePrivate.java | 26 ++++++++++++++ .../java/com/example/PublicInterface.java | 23 ++++++++++++ .../generator/compile/TestCompilerTests.java | 22 +++++++++++- .../test/generator/file/SourceFileTests.java | 8 ----- 16 files changed, 146 insertions(+), 47 deletions(-) create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/package-info.java create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generator/file/package-info.java create mode 100644 spring-core-test/src/test/java/com/example/PackagePrivate.java create mode 100644 spring-core-test/src/test/java/com/example/PublicInterface.java diff --git a/spring-core-test/spring-core-test.gradle b/spring-core-test/spring-core-test.gradle index 5565599898..1324f98ba6 100644 --- a/spring-core-test/spring-core-test.gradle +++ b/spring-core-test/spring-core-test.gradle @@ -1,5 +1,3 @@ -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar - description = "Spring Core Test" dependencies { diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/Compiled.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/Compiled.java index 461397ae36..8b87b54d27 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/Compiled.java +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/Compiled.java @@ -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> compiledClasses; @@ -107,8 +109,7 @@ public class Compiled { * @throws IllegalStateException if no instance can be found or instantiated */ public T getInstance(Class type) { - List> matching = getAllCompiledClasses().stream().filter( - candidate -> type.isAssignableFrom(candidate)).toList(); + List> 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)); diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicClassFileObject.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicClassFileObject.java index 44cf24696f..e1df45396a 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicClassFileObject.java +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicClassFileObject.java @@ -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(); } diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicClassLoader.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicClassLoader.java index 668682b610..a352971fd7 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicClassLoader.java +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicClassLoader.java @@ -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 classFiles; + private final ClassLoader sourceLoader; - public DynamicClassLoader(ClassLoader parent, SourceFiles sourceFiles, + + public DynamicClassLoader(ClassLoader sourceLoader, SourceFiles sourceFiles, ResourceFiles resourceFiles, Map 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 implements Enumeration { + @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; diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicJavaFileManager.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicJavaFileManager.java index cbac149953..66901d08e0 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicJavaFileManager.java +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicJavaFileManager.java @@ -42,7 +42,6 @@ class DynamicJavaFileManager extends ForwardingJavaFileManager new LinkedHashMap<>()); - DynamicJavaFileManager(JavaFileManager fileManager, ClassLoader classLoader) { super(fileManager); this.classLoader = classLoader; diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicJavaFileObject.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicJavaFileObject.java index 92862fc1b2..734d0ed0d7 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicJavaFileObject.java +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicJavaFileObject.java @@ -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(); } diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/TestCompiler.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/TestCompiler.java index f6b459b157..69be84ffd7 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/TestCompiler.java +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/TestCompiler.java @@ -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 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()); } } diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/package-info.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/package-info.java new file mode 100644 index 0000000000..2ca3f06936 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/package-info.java @@ -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; diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/DynamicFile.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/DynamicFile.java index c0c40b88fe..1919884521 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/DynamicFile.java +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/DynamicFile.java @@ -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); diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/DynamicFileAssert.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/DynamicFileAssert.java index 5afc8ba785..bde931302c 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/DynamicFileAssert.java +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/DynamicFileAssert.java @@ -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, 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); } diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/SourceFileAssert.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/SourceFileAssert.java index a8ae7be380..3a5e8e92dd 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/SourceFileAssert.java +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/SourceFileAssert.java @@ -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 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); diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/package-info.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/package-info.java new file mode 100644 index 0000000000..7759f684f8 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/package-info.java @@ -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; diff --git a/spring-core-test/src/test/java/com/example/PackagePrivate.java b/spring-core-test/src/test/java/com/example/PackagePrivate.java new file mode 100644 index 0000000000..56bb0b86ad --- /dev/null +++ b/spring-core-test/src/test/java/com/example/PackagePrivate.java @@ -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"; + } + +} diff --git a/spring-core-test/src/test/java/com/example/PublicInterface.java b/spring-core-test/src/test/java/com/example/PublicInterface.java new file mode 100644 index 0000000000..35a0a959a5 --- /dev/null +++ b/spring-core-test/src/test/java/com/example/PublicInterface.java @@ -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(); + +} diff --git a/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/TestCompilerTests.java b/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/TestCompilerTests.java index d63c1f86db..9bc468f21a 100644 --- a/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/TestCompilerTests.java +++ b/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/TestCompilerTests.java @@ -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!"); } diff --git a/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/SourceFileTests.java b/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/SourceFileTests.java index c22c62de2c..df85fa8065 100644 --- a/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/SourceFileTests.java +++ b/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/SourceFileTests.java @@ -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(