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 <wilkinsona@vmware.com>
This commit is contained in:
@@ -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<String, DynamicClassFileObject> classFiles;
|
||||
private final ClassFiles classFiles;
|
||||
|
||||
@Nullable
|
||||
private final Method defineClassMethod;
|
||||
|
||||
|
||||
public DynamicClassLoader(ClassLoader parent, ResourceFiles resourceFiles,
|
||||
Map<String, DynamicClassFileObject> 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);
|
||||
|
||||
@@ -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<JavaFileManager> {
|
||||
|
||||
private final ClassFiles existingClasses;
|
||||
|
||||
private final ClassLoader classLoader;
|
||||
|
||||
private final Map<String, DynamicClassFileObject> classFiles = Collections.synchronizedMap(
|
||||
private final Map<String, DynamicClassFileObject> 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<JavaFileManager>
|
||||
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<String, DynamicClassFileObject> getClassFiles() {
|
||||
return Collections.unmodifiableMap(this.classFiles);
|
||||
@Override
|
||||
public Iterable<JavaFileObject> list(Location location, String packageName,
|
||||
Set<Kind> kinds, boolean recurse) throws IOException {
|
||||
List<JavaFileObject> 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<JavaFileObject> 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<Processor> processors;
|
||||
|
||||
|
||||
private TestCompiler(@Nullable ClassLoader classLoader, JavaCompiler compiler,
|
||||
SourceFiles sourceFiles, ResourceFiles resourceFiles,
|
||||
SourceFiles sourceFiles, ResourceFiles resourceFiles, ClassFiles classFiles,
|
||||
List<Processor> 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<ResourceFile> resourceFiles = new ArrayList<>();
|
||||
generatedFiles.getGeneratedFiles(Kind.RESOURCE).forEach(
|
||||
(path, inputStreamSource) -> resourceFiles.add(ResourceFile.of(path, inputStreamSource)));
|
||||
return withSources(sourceFiles).withResources(resourceFiles);
|
||||
List<ClassFile> 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<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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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<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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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<ClassFile> 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<Processor> 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<Processor> 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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ClassFile> {
|
||||
|
||||
private static final ClassFiles NONE = new ClassFiles(Collections.emptyMap());
|
||||
|
||||
private final Map<String, ClassFile> files;
|
||||
|
||||
private ClassFiles(Map<String, ClassFile> 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<String, ClassFile> 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<ClassFile> classFiles) {
|
||||
Map<String, ClassFile> merged = new LinkedHashMap<>(this.files);
|
||||
classFiles.forEach(file -> merged.put(file.getName(), file));
|
||||
return new ClassFiles(Collections.unmodifiableMap(merged));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<ClassFile> iterator() {
|
||||
return this.files.values().iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream the {@link ClassFile} instances contained in this collection.
|
||||
* @return a stream of classes
|
||||
*/
|
||||
public Stream<ClassFile> 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<JavaFileObject> 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<JavaFileObject> 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<JavaFileObject> listed = this.fileManager.list(
|
||||
this.location, "com.example", EnumSet.of(Kind.SOURCE), true);
|
||||
assertThat(listed).hasSize(0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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!");
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ClassFile> 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);
|
||||
}
|
||||
|
||||
}
|
||||
BIN
spring-core-test/src/test/resources/com.example.Messages
Normal file
BIN
spring-core-test/src/test/resources/com.example.Messages
Normal file
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user