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 0000000000..ecf3fdf699 Binary files /dev/null and b/spring-core-test/src/test/resources/com.example.Messages differ 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 0000000000..4028f65da4 Binary files /dev/null and b/spring-core-test/src/test/resources/com.example.subpackage.Messages differ