From e59da2de1eeda3a6b9c5c1343e4a321dced8092d Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 15 Sep 2022 15:32:50 +0200 Subject: [PATCH] Allow test compilation to reference existing generated classes Previously, the generated classes from an InMemoryGeneratedFiles were not taken into account and if generated code refers to any of them, compilation failed. This commit introduces a ClasFile abstraction, similar to ResourceFile for resources that represents an existing generated class. Closes gh-29141 Co-authored-by: Andy Wilkinson --- .../generate/compile/DynamicClassLoader.java | 16 +- .../compile/DynamicJavaFileManager.java | 72 ++++++++- .../test/generate/compile/TestCompiler.java | 48 ++++-- .../aot/test/generate/file/ClassFile.java | 105 +++++++++++++ .../aot/test/generate/file/ClassFiles.java | 139 ++++++++++++++++++ .../compile/DynamicJavaFileManagerTests.java | 44 +++++- .../generate/compile/TestCompilerTests.java | 42 ++++++ .../test/generate/file/ClassFileTests.java | 60 ++++++++ .../test/generate/file/ClassFilesTests.java | 116 +++++++++++++++ .../src/test/resources/com.example.Messages | Bin 0 -> 345 bytes .../resources/com.example.subpackage.Messages | Bin 0 -> 383 bytes 11 files changed, 611 insertions(+), 31 deletions(-) create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generate/file/ClassFile.java create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generate/file/ClassFiles.java create mode 100644 spring-core-test/src/test/java/org/springframework/aot/test/generate/file/ClassFileTests.java create mode 100644 spring-core-test/src/test/java/org/springframework/aot/test/generate/file/ClassFilesTests.java create mode 100644 spring-core-test/src/test/resources/com.example.Messages create mode 100644 spring-core-test/src/test/resources/com.example.subpackage.Messages diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/DynamicClassLoader.java b/spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/DynamicClassLoader.java index 3154821cc5..36cf237295 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/DynamicClassLoader.java +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/DynamicClassLoader.java @@ -26,8 +26,9 @@ import java.net.URLConnection; import java.net.URLStreamHandler; import java.nio.charset.StandardCharsets; import java.util.Enumeration; -import java.util.Map; +import org.springframework.aot.test.generate.file.ClassFile; +import org.springframework.aot.test.generate.file.ClassFiles; import org.springframework.aot.test.generate.file.ResourceFile; import org.springframework.aot.test.generate.file.ResourceFiles; import org.springframework.lang.Nullable; @@ -44,14 +45,14 @@ public class DynamicClassLoader extends ClassLoader { private final ResourceFiles resourceFiles; - private final Map classFiles; + private final ClassFiles classFiles; @Nullable private final Method defineClassMethod; public DynamicClassLoader(ClassLoader parent, ResourceFiles resourceFiles, - Map classFiles) { + ClassFiles classFiles) { super(parent); this.resourceFiles = resourceFiles; @@ -77,15 +78,16 @@ public class DynamicClassLoader extends ClassLoader { @Override protected Class findClass(String name) throws ClassNotFoundException { - DynamicClassFileObject classFile = this.classFiles.get(name); + ClassFile classFile = this.classFiles.get(name); if (classFile != null) { - return defineClass(name, classFile); + return defineClass(classFile); } return super.findClass(name); } - private Class defineClass(String name, DynamicClassFileObject classFile) { - byte[] bytes = classFile.getBytes(); + private Class defineClass(ClassFile classFile) { + String name = classFile.getName(); + byte[] bytes = classFile.getContent(); if (this.defineClassMethod != null) { return (Class) ReflectionUtils.invokeMethod(this.defineClassMethod, getParent(), name, bytes, 0, bytes.length); diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/DynamicJavaFileManager.java b/spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/DynamicJavaFileManager.java index 48ba1afcad..80e56a97a6 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/DynamicJavaFileManager.java +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/DynamicJavaFileManager.java @@ -16,35 +16,51 @@ package org.springframework.aot.test.generate.compile; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.Set; import javax.tools.FileObject; import javax.tools.ForwardingJavaFileManager; import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; +import javax.tools.JavaFileObject.Kind; +import javax.tools.SimpleJavaFileObject; + +import org.springframework.aot.test.generate.file.ClassFile; +import org.springframework.aot.test.generate.file.ClassFiles; +import org.springframework.util.ClassUtils; /** * {@link JavaFileManager} to create in-memory {@link DynamicClassFileObject * ClassFileObjects} when compiling. * * @author Phillip Webb + * @author Andy Wilkinson * @since 6.0 */ class DynamicJavaFileManager extends ForwardingJavaFileManager { + private final ClassFiles existingClasses; private final ClassLoader classLoader; - private final Map classFiles = Collections.synchronizedMap( + private final Map compiledClasses = Collections.synchronizedMap( new LinkedHashMap<>()); - DynamicJavaFileManager(JavaFileManager fileManager, ClassLoader classLoader) { + DynamicJavaFileManager(JavaFileManager fileManager, ClassLoader classLoader, + ClassFiles existingClasses) { super(fileManager); this.classLoader = classLoader; + this.existingClasses = existingClasses; } @@ -57,14 +73,60 @@ class DynamicJavaFileManager extends ForwardingJavaFileManager public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException { if (kind == JavaFileObject.Kind.CLASS) { - return this.classFiles.computeIfAbsent(className, + return this.compiledClasses.computeIfAbsent(className, DynamicClassFileObject::new); } return super.getJavaFileForOutput(location, className, kind, sibling); } - Map getClassFiles() { - return Collections.unmodifiableMap(this.classFiles); + @Override + public Iterable list(Location location, String packageName, + Set kinds, boolean recurse) throws IOException { + List result = new ArrayList<>(); + if (kinds.contains(Kind.CLASS)) { + for (ClassFile existingClass : this.existingClasses) { + String existingPackageName = ClassUtils.getPackageName(existingClass.getName()); + if (existingPackageName.equals(packageName) || (recurse && existingPackageName.startsWith(packageName + "."))) { + result.add(new ClassFileJavaFileObject(existingClass)); + } + } + } + Iterable listed = super.list(location, packageName, kinds, recurse); + listed.forEach(result::add); + return result; + } + + @Override + public String inferBinaryName(Location location, JavaFileObject file) { + if (file instanceof ClassFileJavaFileObject classFile) { + return classFile.getClassName(); + } + return super.inferBinaryName(location, file); + } + + ClassFiles getClassFiles() { + return this.existingClasses.and(this.compiledClasses.entrySet().stream().map(entry -> + ClassFile.of(entry.getKey(), entry.getValue().getBytes())).toList()); + } + + private static final class ClassFileJavaFileObject extends SimpleJavaFileObject { + + private final ClassFile classFile; + + private ClassFileJavaFileObject(ClassFile classFile) { + super(URI.create("class:///" + classFile.getName().replace('.', '/') + ".class"), Kind.CLASS); + this.classFile = classFile; + } + + public String getClassName() { + return this.classFile.getName(); + } + + @Override + public InputStream openInputStream() { + return new ByteArrayInputStream(this.classFile.getContent()); + } + } } diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/TestCompiler.java b/spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/TestCompiler.java index 8289656076..dbe2f6127b 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/TestCompiler.java +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/TestCompiler.java @@ -35,6 +35,8 @@ import javax.tools.ToolProvider; import org.springframework.aot.generate.GeneratedFiles.Kind; import org.springframework.aot.generate.InMemoryGeneratedFiles; +import org.springframework.aot.test.generate.file.ClassFile; +import org.springframework.aot.test.generate.file.ClassFiles; import org.springframework.aot.test.generate.file.ResourceFile; import org.springframework.aot.test.generate.file.ResourceFiles; import org.springframework.aot.test.generate.file.SourceFile; @@ -61,17 +63,20 @@ public final class TestCompiler { private final ResourceFiles resourceFiles; + private final ClassFiles classFiles; + private final List processors; private TestCompiler(@Nullable ClassLoader classLoader, JavaCompiler compiler, - SourceFiles sourceFiles, ResourceFiles resourceFiles, + SourceFiles sourceFiles, ResourceFiles resourceFiles, ClassFiles classFiles, List processors) { this.classLoader = classLoader; this.compiler = compiler; this.sourceFiles = sourceFiles; this.resourceFiles = resourceFiles; + this.classFiles = classFiles; this.processors = processors; } @@ -91,12 +96,12 @@ public final class TestCompiler { */ public static TestCompiler forCompiler(JavaCompiler javaCompiler) { return new TestCompiler(null, javaCompiler, SourceFiles.none(), - ResourceFiles.none(), Collections.emptyList()); + ResourceFiles.none(), ClassFiles.none(), Collections.emptyList()); } /** * Create a new {@code TestCompiler} instance with additional generated - * source and resource files. + * source, resource, and class files. * @param generatedFiles the generated files to add * @return a new {@code TestCompiler} instance */ @@ -107,7 +112,11 @@ public final class TestCompiler { List resourceFiles = new ArrayList<>(); generatedFiles.getGeneratedFiles(Kind.RESOURCE).forEach( (path, inputStreamSource) -> resourceFiles.add(ResourceFile.of(path, inputStreamSource))); - return withSources(sourceFiles).withResources(resourceFiles); + List classFiles = new ArrayList<>(); + generatedFiles.getGeneratedFiles(Kind.CLASS).forEach( + (path, inputStreamSource) -> classFiles.add(ClassFile.of( + ClassFile.toClassName(path), inputStreamSource))); + return withSources(sourceFiles).withResources(resourceFiles).withClasses(classFiles); } /** @@ -117,7 +126,8 @@ public final class TestCompiler { */ public TestCompiler withSources(SourceFile... sourceFiles) { return new TestCompiler(this.classLoader, this.compiler, - this.sourceFiles.and(sourceFiles), this.resourceFiles, this.processors); + this.sourceFiles.and(sourceFiles), this.resourceFiles, + this.classFiles, this.processors); } /** @@ -127,7 +137,8 @@ public final class TestCompiler { */ public TestCompiler withSources(Iterable sourceFiles) { return new TestCompiler(this.classLoader, this.compiler, - this.sourceFiles.and(sourceFiles), this.resourceFiles, this.processors); + this.sourceFiles.and(sourceFiles), this.resourceFiles, + this.classFiles, this.processors); } /** @@ -137,7 +148,8 @@ public final class TestCompiler { */ public TestCompiler withSources(SourceFiles sourceFiles) { return new TestCompiler(this.classLoader, this.compiler, - this.sourceFiles.and(sourceFiles), this.resourceFiles, this.processors); + this.sourceFiles.and(sourceFiles), this.resourceFiles, + this.classFiles, this.processors); } /** @@ -147,7 +159,7 @@ public final class TestCompiler { */ public TestCompiler withResources(ResourceFile... resourceFiles) { return new TestCompiler(this.classLoader, this.compiler, this.sourceFiles, - this.resourceFiles.and(resourceFiles), this.processors); + this.resourceFiles.and(resourceFiles), this.classFiles, this.processors); } /** @@ -157,7 +169,7 @@ public final class TestCompiler { */ public TestCompiler withResources(Iterable resourceFiles) { return new TestCompiler(this.classLoader, this.compiler, this.sourceFiles, - this.resourceFiles.and(resourceFiles), this.processors); + this.resourceFiles.and(resourceFiles), this.classFiles, this.processors); } /** @@ -167,7 +179,17 @@ public final class TestCompiler { */ public TestCompiler withResources(ResourceFiles resourceFiles) { return new TestCompiler(this.classLoader, this.compiler, this.sourceFiles, - this.resourceFiles.and(resourceFiles), this.processors); + this.resourceFiles.and(resourceFiles), this.classFiles, this.processors); + } + + /** + * Create a new {@code TestCompiler} instance with additional classes. + * @param classFiles the additional classes + * @return a new {@code TestCompiler} instance + */ + public TestCompiler withClasses(Iterable classFiles) { + return new TestCompiler(this.classLoader, this.compiler, this.sourceFiles, + this.resourceFiles, this.classFiles.and(classFiles), this.processors); } /** @@ -179,7 +201,7 @@ public final class TestCompiler { List mergedProcessors = new ArrayList<>(this.processors); mergedProcessors.addAll(Arrays.asList(processors)); return new TestCompiler(this.classLoader, this.compiler, this.sourceFiles, - this.resourceFiles, mergedProcessors); + this.resourceFiles, this.classFiles, mergedProcessors); } /** @@ -191,7 +213,7 @@ public final class TestCompiler { List mergedProcessors = new ArrayList<>(this.processors); processors.forEach(mergedProcessors::add); return new TestCompiler(this.classLoader, this.compiler, this.sourceFiles, - this.resourceFiles, mergedProcessors); + this.resourceFiles, this.classFiles, mergedProcessors); } /** @@ -269,7 +291,7 @@ public final class TestCompiler { StandardJavaFileManager standardFileManager = this.compiler.getStandardFileManager( null, null, null); DynamicJavaFileManager fileManager = new DynamicJavaFileManager( - standardFileManager, classLoaderToUse); + standardFileManager, classLoaderToUse, this.classFiles); if (!this.sourceFiles.isEmpty()) { Errors errors = new Errors(); CompilationTask task = this.compiler.getTask(null, fileManager, errors, null, diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generate/file/ClassFile.java b/spring-core-test/src/main/java/org/springframework/aot/test/generate/file/ClassFile.java new file mode 100644 index 0000000000..f3b680bf7d --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generate/file/ClassFile.java @@ -0,0 +1,105 @@ +/* + * 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 org.springframework.aot.test.generate.file; + +import java.io.IOException; + +import org.springframework.core.io.InputStreamSource; +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; + +/** + * In memory representation of a Java class. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public final class ClassFile { + + private static final String CLASS_SUFFIX = ".class"; + + private final String name; + + private final byte[] content; + + private ClassFile(String name, byte[] content) { + this.name = name; + this.content = content; + } + + /** + * Return the fully qualified name of the class. + * @return the class name + */ + public String getName() { + return this.name; + } + + /** + * Return the bytecode content. + * @return the class content + */ + public byte[] getContent() { + return this.content; + } + + /** + * Factory method to create a new {@link ClassFile} from the given + * {@code content}. + * @param name the fully qualified name of the class + * @param content the bytecode of the class + * @return a {@link ClassFile} instance + */ + public static ClassFile of(String name, byte[] content) { + return new ClassFile(name, content); + } + + /** + * Factory method to create a new {@link ClassFile} from the given + * {@link InputStreamSource}. + * @param name the fully qualified name of the class + * @param inputStreamSource the bytecode of the class + * @return a {@link ClassFile} instance + */ + public static ClassFile of(String name, InputStreamSource inputStreamSource) { + return of(name, toBytes(inputStreamSource)); + } + + /** + * Return the name of a class based on its relative path. + * @param path the path of the class + * @return the class name + */ + public static String toClassName(String path) { + Assert.hasText(path, "'path' must not be empty"); + if (!path.endsWith(CLASS_SUFFIX)) { + throw new IllegalArgumentException("Path '" + path + "' must end with '.class'"); + } + String name = path.replace('/', '.'); + return name.substring(0, name.length() - CLASS_SUFFIX.length()); + } + + private static byte[] toBytes(InputStreamSource inputStreamSource) { + try { + return FileCopyUtils.copyToByteArray(inputStreamSource.getInputStream()); + } + catch (IOException ex) { + throw new IllegalStateException("Unable to read content", ex); + } + } + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generate/file/ClassFiles.java b/spring-core-test/src/main/java/org/springframework/aot/test/generate/file/ClassFiles.java new file mode 100644 index 0000000000..c17759c87e --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generate/file/ClassFiles.java @@ -0,0 +1,139 @@ +/* + * 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 org.springframework.aot.test.generate.file; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Stream; + +import org.springframework.lang.Nullable; + +/** + * An immutable collection of {@link ClassFile} instances. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public final class ClassFiles implements Iterable { + + private static final ClassFiles NONE = new ClassFiles(Collections.emptyMap()); + + private final Map files; + + private ClassFiles(Map files) { + this.files = files; + } + + + /** + * Return a {@link ClassFiles} instance with no items. + * @return the empty instance + */ + public static ClassFiles none() { + return NONE; + } + + /** + * Factory method that can be used to create a {@link ClassFiles} + * instance containing the specified classes. + * @param ClassFiles the classes to include + * @return a {@link ClassFiles} instance + */ + public static ClassFiles of(ClassFile... ClassFiles) { + return none().and(ClassFiles); + } + + /** + * Return a new {@link ClassFiles} instance that merges classes from + * another array of {@link ClassFile} instances. + * @param classFiles the instances to merge + * @return a new {@link ClassFiles} instance containing merged content + */ + public ClassFiles and(ClassFile... classFiles) { + Map merged = new LinkedHashMap<>(this.files); + Arrays.stream(classFiles).forEach(file -> merged.put(file.getName(), file)); + return new ClassFiles(Collections.unmodifiableMap(merged)); + } + + /** + * Return a new {@link ClassFiles} instance that merges classes from another + * iterable of {@link ClassFiles} instances. + * @param classFiles the instances to merge + * @return a new {@link ClassFiles} instance containing merged content + */ + public ClassFiles and(Iterable classFiles) { + Map merged = new LinkedHashMap<>(this.files); + classFiles.forEach(file -> merged.put(file.getName(), file)); + return new ClassFiles(Collections.unmodifiableMap(merged)); + } + + @Override + public Iterator iterator() { + return this.files.values().iterator(); + } + + /** + * Stream the {@link ClassFile} instances contained in this collection. + * @return a stream of classes + */ + public Stream stream() { + return this.files.values().stream(); + } + + /** + * Returns {@code true} if this collection is empty. + * @return if this collection is empty + */ + public boolean isEmpty() { + return this.files.isEmpty(); + } + + /** + * Get the {@link ClassFile} with the given class name. + * @param name the fully qualified name to find + * @return a {@link ClassFile} instance or {@code null} + */ + @Nullable + public ClassFile get(String name) { + return this.files.get(name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return this.files.equals(((ClassFiles) obj).files); + } + + @Override + public int hashCode() { + return this.files.hashCode(); + } + + @Override + public String toString() { + return this.files.toString(); + } + +} diff --git a/spring-core-test/src/test/java/org/springframework/aot/test/generate/compile/DynamicJavaFileManagerTests.java b/spring-core-test/src/test/java/org/springframework/aot/test/generate/compile/DynamicJavaFileManagerTests.java index 75c5ba2f78..6faa4729b9 100644 --- a/spring-core-test/src/test/java/org/springframework/aot/test/generate/compile/DynamicJavaFileManagerTests.java +++ b/spring-core-test/src/test/java/org/springframework/aot/test/generate/compile/DynamicJavaFileManagerTests.java @@ -16,6 +16,9 @@ package org.springframework.aot.test.generate.compile; +import java.io.IOException; +import java.util.EnumSet; + import javax.tools.JavaFileManager; import javax.tools.JavaFileManager.Location; import javax.tools.JavaFileObject; @@ -26,6 +29,9 @@ import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.springframework.aot.test.generate.file.ClassFile; +import org.springframework.aot.test.generate.file.ClassFiles; + import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.then; @@ -36,6 +42,8 @@ import static org.mockito.BDDMockito.then; */ class DynamicJavaFileManagerTests { + private static final byte[] DUMMY_BYTECODE = new byte[] { 'a' }; + @Mock private JavaFileManager parentFileManager; @@ -49,10 +57,10 @@ class DynamicJavaFileManagerTests { @BeforeEach void setup() { MockitoAnnotations.openMocks(this); - this.classLoader = new ClassLoader() { - }; - this.fileManager = new DynamicJavaFileManager(this.parentFileManager, - this.classLoader); + this.classLoader = new ClassLoader() {}; + this.fileManager = new DynamicJavaFileManager(this.parentFileManager, this.classLoader, + ClassFiles.of(ClassFile.of("com.example.one.ClassOne", DUMMY_BYTECODE), + ClassFile.of("com.example.two.ClassTwo", DUMMY_BYTECODE))); } @Test @@ -93,8 +101,32 @@ class DynamicJavaFileManagerTests { Kind.CLASS, null); this.fileManager.getJavaFileForOutput(this.location, "com.example.MyClass2", Kind.CLASS, null); - assertThat(this.fileManager.getClassFiles()).containsKeys("com.example.MyClass1", - "com.example.MyClass2"); + assertThat(this.fileManager.getClassFiles().stream().map(ClassFile::getName)) + .contains("com.example.MyClass1", "com.example.MyClass2"); + } + + @Test + void listWithoutRecurseReturnsClassesInRequestedPackage() throws IOException { + Iterable listed = this.fileManager.list( + this.location, "com.example.one", EnumSet.allOf(Kind.class), false); + assertThat(listed).hasSize(1); + assertThat(listed).extracting(JavaFileObject::getName).containsExactly("/com/example/one/ClassOne.class"); + } + + @Test + void listWithRecurseReturnsClassesInRequestedPackageAndSubpackages() throws IOException { + Iterable listed = this.fileManager.list( + this.location, "com.example", EnumSet.allOf(Kind.class), true); + assertThat(listed).hasSize(2); + assertThat(listed).extracting(JavaFileObject::getName) + .containsExactly("/com/example/one/ClassOne.class", "/com/example/two/ClassTwo.class"); + } + + @Test + void listWithoutClassKindDoesNotReturnClasses() throws IOException { + Iterable listed = this.fileManager.list( + this.location, "com.example", EnumSet.of(Kind.SOURCE), true); + assertThat(listed).hasSize(0); } } diff --git a/spring-core-test/src/test/java/org/springframework/aot/test/generate/compile/TestCompilerTests.java b/spring-core-test/src/test/java/org/springframework/aot/test/generate/compile/TestCompilerTests.java index 299bc85579..0eac02675c 100644 --- a/spring-core-test/src/test/java/org/springframework/aot/test/generate/compile/TestCompilerTests.java +++ b/spring-core-test/src/test/java/org/springframework/aot/test/generate/compile/TestCompilerTests.java @@ -30,11 +30,13 @@ import javax.lang.model.element.TypeElement; import com.example.PublicInterface; import org.junit.jupiter.api.Test; +import org.springframework.aot.test.generate.file.ClassFile; import org.springframework.aot.test.generate.file.ResourceFile; import org.springframework.aot.test.generate.file.ResourceFiles; import org.springframework.aot.test.generate.file.SourceFile; import org.springframework.aot.test.generate.file.SourceFiles; import org.springframework.aot.test.generate.file.WritableContent; +import org.springframework.core.io.ClassPathResource; import org.springframework.util.ClassUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -241,6 +243,46 @@ class TestCompilerTests { .withMessageContaining(ClassUtils.getShortName(CompileWithTargetClassAccess.class)); } + @Test + void compiledCodeCanReferenceAdditionalClassInSamePackage() { + SourceFiles sourceFiles = SourceFiles.of(SourceFile.of(""" + package com.example; + + public class Test implements PublicInterface { + + public String perform() { + return Messages.HELLO; + } + + } + """)); + ClassFile messagesClass = ClassFile.of("com.example.Messages", + new ClassPathResource("com.example.Messages")); + TestCompiler.forSystem().withClasses(List.of(messagesClass)).compile(sourceFiles, compiled -> + assertThat(compiled.getInstance(PublicInterface.class, "com.example.Test").perform()) + .isEqualTo("Hello")); + } + + @Test + void compiledCodeCanReferenceAdditionalClassInDifferentPackage() { + SourceFiles sourceFiles = SourceFiles.of(SourceFile.of(""" + package com.example; + + import com.example.subpackage.Messages; + + public class Test implements PublicInterface { + + public String perform() { + return Messages.HELLO; + } + + } + """)); + ClassFile messagesClass = ClassFile.of("com.example.subpackage.Messages", + new ClassPathResource("com.example.subpackage.Messages")); + TestCompiler.forSystem().withClasses(List.of(messagesClass)).compile(sourceFiles, compiled -> assertThat( + compiled.getInstance(PublicInterface.class, "com.example.Test").perform()).isEqualTo("Hello from subpackage")); + } 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/generate/file/ClassFileTests.java b/spring-core-test/src/test/java/org/springframework/aot/test/generate/file/ClassFileTests.java new file mode 100644 index 0000000000..cb18a4330e --- /dev/null +++ b/spring-core-test/src/test/java/org/springframework/aot/test/generate/file/ClassFileTests.java @@ -0,0 +1,60 @@ +/* + * 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 org.springframework.aot.test.generate.file; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ByteArrayResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link ClassFile}. + * + * @author Stephane Nicoll + */ +class ClassFileTests { + + private final static byte[] TEST_CONTENT = new byte[]{'a'}; + + @Test + void ofNameAndByteArrayCreatesClass() { + ClassFile classFile = ClassFile.of("com.example.Test", TEST_CONTENT); + assertThat(classFile.getName()).isEqualTo("com.example.Test"); + assertThat(classFile.getContent()).isEqualTo(TEST_CONTENT); + } + + @Test + void ofNameAndInputStreamResourceCreatesClass() { + ClassFile classFile = ClassFile.of("com.example.Test", + new ByteArrayResource(TEST_CONTENT)); + assertThat(classFile.getName()).isEqualTo("com.example.Test"); + assertThat(classFile.getContent()).isEqualTo(TEST_CONTENT); + } + + @Test + void toClassNameWithPathToClassFile() { + assertThat(ClassFile.toClassName("com/example/Test.class")).isEqualTo("com.example.Test"); + } + + @Test + void toClassNameWithPathToTextFile() { + assertThatIllegalArgumentException().isThrownBy(() -> ClassFile.toClassName("com/example/Test.txt")); + } + +} diff --git a/spring-core-test/src/test/java/org/springframework/aot/test/generate/file/ClassFilesTests.java b/spring-core-test/src/test/java/org/springframework/aot/test/generate/file/ClassFilesTests.java new file mode 100644 index 0000000000..fd46e2602e --- /dev/null +++ b/spring-core-test/src/test/java/org/springframework/aot/test/generate/file/ClassFilesTests.java @@ -0,0 +1,116 @@ +/* + * 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 org.springframework.aot.test.generate.file; + +import java.util.Iterator; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatObject; + +/** + * Tests for {@link ClassFiles}. + * + * @author Stephane Nicoll + */ +class ClassFilesTests { + + private static final ClassFile CLASS_FILE_1 = ClassFile.of( + "com.example.Test1", new byte[] { 'a' }); + + private static final ClassFile CLASS_FILE_2 = ClassFile.of( + "com.example.Test2", new byte[] { 'b' }); + + @Test + void noneReturnsNone() { + ClassFiles none = ClassFiles.none(); + assertThat(none).isNotNull(); + assertThat(none.isEmpty()).isTrue(); + } + + @Test + void ofCreatesClassFiles() { + ClassFiles classFiles = ClassFiles.of(CLASS_FILE_1, CLASS_FILE_2); + assertThat(classFiles).containsExactly(CLASS_FILE_1, CLASS_FILE_2); + } + + @Test + void andAddsClassFiles() { + ClassFiles classFiles = ClassFiles.of(CLASS_FILE_1); + ClassFiles added = classFiles.and(CLASS_FILE_2); + assertThat(classFiles).containsExactly(CLASS_FILE_1); + assertThat(added).containsExactly(CLASS_FILE_1, CLASS_FILE_2); + } + + @Test + void andClassFilesAddsClassFiles() { + ClassFiles classFiles = ClassFiles.of(CLASS_FILE_1); + ClassFiles added = classFiles.and(ClassFiles.of(CLASS_FILE_2)); + assertThat(classFiles).containsExactly(CLASS_FILE_1); + assertThat(added).containsExactly(CLASS_FILE_1, CLASS_FILE_2); + } + + @Test + void iteratorIteratesClassFiles() { + ClassFiles classFiles = ClassFiles.of(CLASS_FILE_1, CLASS_FILE_2); + Iterator iterator = classFiles.iterator(); + assertThat(iterator.next()).isEqualTo(CLASS_FILE_1); + assertThat(iterator.next()).isEqualTo(CLASS_FILE_2); + assertThat(iterator.hasNext()).isFalse(); + } + + @Test + void streamStreamsClassFiles() { + ClassFiles classFiles = ClassFiles.of(CLASS_FILE_1, CLASS_FILE_2); + assertThat(classFiles.stream()).containsExactly(CLASS_FILE_1, CLASS_FILE_2); + } + + @Test + void isEmptyWhenEmptyReturnsTrue() { + ClassFiles classFiles = ClassFiles.of(); + assertThat(classFiles.isEmpty()).isTrue(); + } + + @Test + void isEmptyWhenNotEmptyReturnsFalse() { + ClassFiles classFiles = ClassFiles.of(CLASS_FILE_1); + assertThat(classFiles.isEmpty()).isFalse(); + } + + @Test + void getWhenHasFileReturnsFile() { + ClassFiles classFiles = ClassFiles.of(CLASS_FILE_1); + assertThat(classFiles.get("com.example.Test1")).isNotNull(); + } + + @Test + void getWhenMissingFileReturnsNull() { + ClassFiles classFiles = ClassFiles.of(CLASS_FILE_2); + assertThatObject(classFiles.get("com.example.another.Test2")).isNull(); + } + + @Test + void equalsAndHashCode() { + ClassFiles s1 = ClassFiles.of(CLASS_FILE_1, CLASS_FILE_2); + ClassFiles s2 = ClassFiles.of(CLASS_FILE_1, CLASS_FILE_2); + ClassFiles s3 = ClassFiles.of(CLASS_FILE_1); + assertThat(s1.hashCode()).isEqualTo(s2.hashCode()); + assertThatObject(s1).isEqualTo(s2).isNotEqualTo(s3); + } + +} diff --git a/spring-core-test/src/test/resources/com.example.Messages b/spring-core-test/src/test/resources/com.example.Messages new file mode 100644 index 0000000000000000000000000000000000000000..ecf3fdf69953201806a77de2ce95f0ac461b0997 GIT binary patch literal 345 zcmZusyH3ME5S&fyI5;*5As|uG0R{d59!&(IFr)}_e>Nv`(b7J_@mC zfoN!EcXoGnXFtE*KLA`{FTjAXC`^^e7g0TFncT{@6{Tzgc!b$TJc~q&x=dF2Mi!Rf z-(02XiZD?L7%bes{qrFoDchS-h} zVnUdvs+Mf`rOA3=FAAYi~}sb0drqS AvH$=8 literal 0 HcmV?d00001 diff --git a/spring-core-test/src/test/resources/com.example.subpackage.Messages b/spring-core-test/src/test/resources/com.example.subpackage.Messages new file mode 100644 index 0000000000000000000000000000000000000000..4028f65da4da3d48e077ca62915dcc85668f0154 GIT binary patch literal 383 zcmah_yH3ME5S&fyI5;*5ffQ6E9Z=v8;L${&6owQ*?$72h7tX%O55#X#AyM!Fd=z4B zfl|`U?#$gjX1~5aJ^@@};-Et~N=z2WSCMVB49Ys*h~!zMGPspx$+2``5k_nAA_6V) zG+4%KnN$ROaUF(B!Z_T%t*S!h={dolo4l+p&I@xTGf*(3A6AYJG`8` zGIf#28`U)P|9qY`HZTJ(!q*`j!bXpy!>ofoXIzN^qsL0na=<=)hyT`o88S8nz&=Kd QUYpz4!