From 653dc5951d24fc2c5560cb38314f536a67c3bbbf Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 24 Feb 2022 21:57:11 -0800 Subject: [PATCH] Add module to support testing of generated code Add a new unpublished `spring-core-test` module to support testing of generated code. The module include a `TestCompiler` class which can be used to dynamically compile generated Java code. It also include an AssertJ friendly `SourceFile` class which uses qdox to provide targeted assertions on specific parts of a generated source file. See gh-28120 --- build.gradle | 1 + framework-bom/framework-bom.gradle | 2 +- settings.gradle | 1 + spring-core-test/spring-core-test.gradle | 13 + .../compile/CompilationException.java | 32 +++ .../aot/test/generator/compile/Compiled.java | 169 ++++++++++++ .../compile/DynamicClassFileObject.java | 63 +++++ .../generator/compile/DynamicClassLoader.java | 186 ++++++++++++++ .../compile/DynamicJavaFileManager.java | 71 +++++ .../compile/DynamicJavaFileObject.java | 50 ++++ .../test/generator/compile/TestCompiler.java | 242 ++++++++++++++++++ .../aot/test/generator/file/DynamicFile.java | 105 ++++++++ .../generator/file/DynamicFileAssert.java | 54 ++++ .../aot/test/generator/file/DynamicFiles.java | 114 +++++++++ .../aot/test/generator/file/MethodAssert.java | 63 +++++ .../aot/test/generator/file/ResourceFile.java | 72 ++++++ .../generator/file/ResourceFileAssert.java | 34 +++ .../test/generator/file/ResourceFiles.java | 144 +++++++++++ .../aot/test/generator/file/SourceFile.java | 161 ++++++++++++ .../test/generator/file/SourceFileAssert.java | 128 +++++++++ .../aot/test/generator/file/SourceFiles.java | 144 +++++++++++ .../test/generator/file/WritableContent.java | 39 +++ .../compile/CompilationExceptionTests.java | 38 +++ .../test/generator/compile/CompiledTests.java | 194 ++++++++++++++ .../compile/DynamicClassFileObjectTests.java | 60 +++++ .../compile/DynamicJavaFileManagerTests.java | 101 ++++++++ .../compile/DynamicJavaFileObjectTests.java | 47 ++++ .../generator/compile/TestCompilerTests.java | 179 +++++++++++++ .../generator/file/MethodAssertTests.java | 73 ++++++ .../generator/file/ResourceFileTests.java | 52 ++++ .../generator/file/ResourceFilesTests.java | 131 ++++++++++ .../generator/file/SourceFileAssertTests.java | 128 +++++++++ .../test/generator/file/SourceFileTests.java | 138 ++++++++++ .../test/generator/file/SourceFilesTests.java | 135 ++++++++++ 34 files changed, 3163 insertions(+), 1 deletion(-) create mode 100644 spring-core-test/spring-core-test.gradle create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/CompilationException.java create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/Compiled.java create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicClassFileObject.java create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicClassLoader.java create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicJavaFileManager.java create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicJavaFileObject.java create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/TestCompiler.java create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generator/file/DynamicFile.java create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generator/file/DynamicFileAssert.java create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generator/file/DynamicFiles.java create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generator/file/MethodAssert.java create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generator/file/ResourceFile.java create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generator/file/ResourceFileAssert.java create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generator/file/ResourceFiles.java create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generator/file/SourceFile.java create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generator/file/SourceFileAssert.java create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generator/file/SourceFiles.java create mode 100644 spring-core-test/src/main/java/org/springframework/aot/test/generator/file/WritableContent.java create mode 100644 spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/CompilationExceptionTests.java create mode 100644 spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/CompiledTests.java create mode 100644 spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/DynamicClassFileObjectTests.java create mode 100644 spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/DynamicJavaFileManagerTests.java create mode 100644 spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/DynamicJavaFileObjectTests.java create mode 100644 spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/TestCompilerTests.java create mode 100644 spring-core-test/src/test/java/org/springframework/aot/test/generator/file/MethodAssertTests.java create mode 100644 spring-core-test/src/test/java/org/springframework/aot/test/generator/file/ResourceFileTests.java create mode 100644 spring-core-test/src/test/java/org/springframework/aot/test/generator/file/ResourceFilesTests.java create mode 100644 spring-core-test/src/test/java/org/springframework/aot/test/generator/file/SourceFileAssertTests.java create mode 100644 spring-core-test/src/test/java/org/springframework/aot/test/generator/file/SourceFileTests.java create mode 100644 spring-core-test/src/test/java/org/springframework/aot/test/generator/file/SourceFilesTests.java diff --git a/build.gradle b/build.gradle index c73b7d5b79b..93b5536e397 100644 --- a/build.gradle +++ b/build.gradle @@ -74,6 +74,7 @@ configure(allprojects) { project -> dependency "com.google.code.gson:gson:2.8.9" dependency "com.google.protobuf:protobuf-java-util:3.19.3" dependency "com.googlecode.protobuf-java-format:protobuf-java-format:1.4" + dependency "com.thoughtworks.qdox:qdox:2.0.1" dependency("com.thoughtworks.xstream:xstream:1.4.18") { exclude group: "xpp3", name: "xpp3_min" exclude group: "xmlpull", name: "xmlpull" diff --git a/framework-bom/framework-bom.gradle b/framework-bom/framework-bom.gradle index 840f20537fa..05e0c4d45ce 100644 --- a/framework-bom/framework-bom.gradle +++ b/framework-bom/framework-bom.gradle @@ -7,7 +7,7 @@ group = "org.springframework" dependencies { constraints { - parent.moduleProjects.sort { "$it.name" }.each { + parent.moduleProjects.findAll{ it.name != 'spring-core-test' }.sort{ "$it.name" }.each { api it } } diff --git a/settings.gradle b/settings.gradle index e1638d26c9a..c57435a3b83 100644 --- a/settings.gradle +++ b/settings.gradle @@ -18,6 +18,7 @@ include "spring-context" include "spring-context-indexer" include "spring-context-support" include "spring-core" +include "spring-core-test" include "spring-expression" include "spring-instrument" include "spring-jcl" diff --git a/spring-core-test/spring-core-test.gradle b/spring-core-test/spring-core-test.gradle new file mode 100644 index 00000000000..55655998983 --- /dev/null +++ b/spring-core-test/spring-core-test.gradle @@ -0,0 +1,13 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +description = "Spring Core Test" + +dependencies { + api(project(":spring-core")) + api("org.assertj:assertj-core") + api("com.thoughtworks.qdox:qdox") +} + +tasks.withType(PublishToMavenRepository).configureEach { + it.enabled = false +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/CompilationException.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/CompilationException.java new file mode 100644 index 00000000000..fd3f9919be5 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/CompilationException.java @@ -0,0 +1,32 @@ +/* + * 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.generator.compile; + +/** + * Exception thrown when code cannot compile. + * + * @author Phillip Webb + * @since 6.0 + */ +@SuppressWarnings("serial") +public class CompilationException extends RuntimeException { + + CompilationException(String message) { + super(message); + } + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/Compiled.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/Compiled.java new file mode 100644 index 00000000000..461397ae362 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/Compiled.java @@ -0,0 +1,169 @@ +/* + * 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.generator.compile; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.aot.test.generator.file.ResourceFile; +import org.springframework.aot.test.generator.file.ResourceFiles; +import org.springframework.aot.test.generator.file.SourceFile; +import org.springframework.aot.test.generator.file.SourceFiles; +import org.springframework.util.Assert; + +/** + * Fully compiled results provided from a {@link TestCompiler}. + * + * @author Phillip Webb + * @since 6.0 + */ +public class Compiled { + + + private final ClassLoader classLoader; + + private final SourceFiles sourceFiles; + + private final ResourceFiles resourceFiles; + + private List> compiledClasses; + + + Compiled(ClassLoader classLoader, SourceFiles sourceFiles, + ResourceFiles resourceFiles) { + this.classLoader = classLoader; + this.sourceFiles = sourceFiles; + this.resourceFiles = resourceFiles; + } + + + /** + * Return the classloader containing the compiled content and access to the + * resources. + * @return the classLoader + */ + public ClassLoader getClassLoader() { + return this.classLoader; + } + + /** + * Return the single source file that was compiled. + * @return the single source file + * @throws IllegalStateException if the compiler wasn't passed exactly one + * file + */ + public SourceFile getSourceFile() { + return this.sourceFiles.getSingle(); + } + + /** + * Return all source files that were compiled. + * @return the source files used by the compiler + */ + public SourceFiles getSourceFiles() { + return this.sourceFiles; + } + + /** + * Return the single resource file that was used when compiled. + * @return the single resource file + * @throws IllegalStateException if the compiler wasn't passed exactly one + * file + */ + public ResourceFile getResourceFile() { + return this.resourceFiles.getSingle(); + } + + /** + * Return all resource files that were compiled. + * @return the resource files used by the compiler + */ + public ResourceFiles getResourceFiles() { + return this.resourceFiles; + } + + /** + * Return a new instance of a compiled class of the given type. There must + * be only a single instance and it must have a default constructor. + * @param the required type + * @param type the required type + * @return an instance of type created from the compiled classes + * @throws IllegalStateException if no instance can be found or instantiated + */ + public T getInstance(Class type) { + List> matching = getAllCompiledClasses().stream().filter( + candidate -> type.isAssignableFrom(candidate)).toList(); + Assert.state(!matching.isEmpty(), () -> "No instance found of type " + type.getName()); + Assert.state(matching.size() == 1, () -> "Multiple instances found of type " + type.getName()); + return newInstance(matching.get(0)); + } + + /** + * Return an instance of a compiled class identified by its class name. The + * class must have a default constructor. + * @param the type to return + * @param type the type to return + * @param className the class name to load + * @return an instance of the class + * @throws IllegalStateException if no instance can be found or instantiated + */ + public T getInstance(Class type, String className) { + Class loaded = loadClass(className); + return newInstance(loaded); + } + + /** + * Return all compiled classes. + * @return a list of all compiled classes + */ + public List> getAllCompiledClasses() { + List> compiledClasses = this.compiledClasses; + if (compiledClasses == null) { + compiledClasses = new ArrayList<>(); + this.sourceFiles.stream().map(this::loadClass).forEach(compiledClasses::add); + this.compiledClasses = Collections.unmodifiableList(compiledClasses); + } + return compiledClasses; + } + + @SuppressWarnings("unchecked") + private T newInstance(Class loaded) { + try { + Constructor constructor = loaded.getDeclaredConstructor(); + return (T) constructor.newInstance(); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + private Class loadClass(SourceFile sourceFile) { + return loadClass(sourceFile.getClassName()); + } + + private Class loadClass(String className) { + try { + return this.classLoader.loadClass(className); + } + catch (ClassNotFoundException ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicClassFileObject.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicClassFileObject.java new file mode 100644 index 00000000000..44cf24696f4 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicClassFileObject.java @@ -0,0 +1,63 @@ +/* + * 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.generator.compile; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; + +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; + +/** + * In-memory {@link JavaFileObject} used to hold class bytecode. + * + * @author Phillip Webb + * @since 6.0 + */ +class DynamicClassFileObject extends SimpleJavaFileObject { + + private volatile byte[] bytes; + + + DynamicClassFileObject(String className) { + super(URI.create("class:///" + className.replace('.', '/') + ".class"), + Kind.CLASS); + } + + + @Override + public OutputStream openOutputStream() throws IOException { + return new JavaClassOutputStream(); + } + + byte[] getBytes() { + return this.bytes; + } + + + class JavaClassOutputStream extends ByteArrayOutputStream { + + @Override + public void close() throws IOException { + DynamicClassFileObject.this.bytes = toByteArray(); + } + + } + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicClassLoader.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicClassLoader.java new file mode 100644 index 00000000000..668682b610d --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicClassLoader.java @@ -0,0 +1,186 @@ +/* + * 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.generator.compile; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.net.MalformedURLException; +import java.net.URL; +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.generator.file.ResourceFile; +import org.springframework.aot.test.generator.file.ResourceFiles; +import org.springframework.aot.test.generator.file.SourceFile; +import org.springframework.aot.test.generator.file.SourceFiles; + +/** + * {@link ClassLoader} used to expose dynamically generated content. + * + * @author Phillip Webb + * @since 6.0 + */ +public class DynamicClassLoader extends ClassLoader { + + private static final Logger logger = System.getLogger( + DynamicClassLoader.class.getName()); + + + private final SourceFiles sourceFiles; + + private final ResourceFiles resourceFiles; + + private final Map classFiles; + + + public DynamicClassLoader(ClassLoader parent, SourceFiles sourceFiles, + ResourceFiles resourceFiles, Map classFiles) { + super(parent); + this.sourceFiles = sourceFiles; + this.resourceFiles = resourceFiles; + this.classFiles = classFiles; + } + + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + DynamicClassFileObject classFile = this.classFiles.get(name); + if (classFile != null) { + return defineClass(name, classFile); + } + return super.findClass(name); + } + + private Class defineClass(String name, DynamicClassFileObject classFile) { + byte[] bytes = classFile.getBytes(); + SourceFile sourceFile = this.sourceFiles.get(name); + if (sourceFile != null && sourceFile.getTarget() != null) { + try { + Lookup lookup = MethodHandles.privateLookupIn(sourceFile.getTarget(), + MethodHandles.lookup()); + return lookup.defineClass(bytes); + } + catch (IllegalAccessException ex) { + logger.log(Level.WARNING, + "Unable to define class using MethodHandles Lookup, " + + "only public methods and classes will be accessible"); + } + } + return defineClass(name, bytes, 0, bytes.length, null); + } + + @Override + protected Enumeration findResources(String name) throws IOException { + URL resource = findResource(name); + if (resource != null) { + return new SingletonEnumeration<>(resource); + } + return super.findResources(name); + } + + @Override + protected URL findResource(String name) { + ResourceFile file = this.resourceFiles.get(name); + if (file != null) { + try { + return new URL(null, "resource:///" + file.getPath(), + new ResourceFileHandler(file)); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + return super.findResource(name); + } + + + private static class SingletonEnumeration implements Enumeration { + + private E element; + + + SingletonEnumeration(E element) { + this.element = element; + } + + + @Override + public boolean hasMoreElements() { + return this.element != null; + } + + @Override + public E nextElement() { + E next = this.element; + this.element = null; + return next; + } + + } + + + private static class ResourceFileHandler extends URLStreamHandler { + + private final ResourceFile file; + + + ResourceFileHandler(ResourceFile file) { + this.file = file; + } + + + @Override + protected URLConnection openConnection(URL url) throws IOException { + return new ResourceFileConnection(url, this.file); + } + + } + + + private static class ResourceFileConnection extends URLConnection { + + private final ResourceFile file; + + + protected ResourceFileConnection(URL url, ResourceFile file) { + super(url); + this.file = file; + } + + + @Override + public void connect() throws IOException { + } + + @Override + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream( + this.file.getContent().getBytes(StandardCharsets.UTF_8)); + + } + + } + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicJavaFileManager.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicJavaFileManager.java new file mode 100644 index 00000000000..cbac1499534 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicJavaFileManager.java @@ -0,0 +1,71 @@ +/* + * 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.generator.compile; + +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.tools.FileObject; +import javax.tools.ForwardingJavaFileManager; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; + +/** + * {@link JavaFileManager} to create in-memory {@link DynamicClassFileObject + * ClassFileObjects} when compiling. + * + * @author Phillip Webb + * @since 6.0 + */ +class DynamicJavaFileManager extends ForwardingJavaFileManager { + + + private final ClassLoader classLoader; + + private final Map classFiles = Collections.synchronizedMap( + new LinkedHashMap<>()); + + + + DynamicJavaFileManager(JavaFileManager fileManager, ClassLoader classLoader) { + super(fileManager); + this.classLoader = classLoader; + } + + + @Override + public ClassLoader getClassLoader(Location location) { + return this.classLoader; + } + + @Override + public JavaFileObject getJavaFileForOutput(Location location, String className, + JavaFileObject.Kind kind, FileObject sibling) throws IOException { + if (kind == JavaFileObject.Kind.CLASS) { + return this.classFiles.computeIfAbsent(className, + DynamicClassFileObject::new); + } + return super.getJavaFileForOutput(location, className, kind, sibling); + } + + Map getClassFiles() { + return Collections.unmodifiableMap(this.classFiles); + } + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicJavaFileObject.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicJavaFileObject.java new file mode 100644 index 00000000000..92862fc1b2e --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/DynamicJavaFileObject.java @@ -0,0 +1,50 @@ +/* + * 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.generator.compile; + +import java.io.IOException; +import java.net.URI; + +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; + +import org.springframework.aot.test.generator.file.SourceFile; + +/** + * Adapts a {@link SourceFile} instance to a {@link JavaFileObject}. + * + * @author Phillip Webb + * @since 6.0 + */ +class DynamicJavaFileObject extends SimpleJavaFileObject { + + + private final SourceFile sourceFile; + + + DynamicJavaFileObject(SourceFile sourceFile) { + super(URI.create(sourceFile.getPath()), Kind.SOURCE); + this.sourceFile = sourceFile; + } + + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { + return this.sourceFile.getContent(); + } + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/TestCompiler.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/TestCompiler.java new file mode 100644 index 00000000000..f6b459b1576 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/compile/TestCompiler.java @@ -0,0 +1,242 @@ +/* + * 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.generator.compile; + +import java.util.List; +import java.util.Locale; +import java.util.function.Consumer; + +import javax.tools.Diagnostic; +import javax.tools.DiagnosticListener; +import javax.tools.JavaCompiler; +import javax.tools.JavaCompiler.CompilationTask; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; + +import org.springframework.aot.test.generator.file.ResourceFile; +import org.springframework.aot.test.generator.file.ResourceFiles; +import org.springframework.aot.test.generator.file.SourceFile; +import org.springframework.aot.test.generator.file.SourceFiles; +import org.springframework.aot.test.generator.file.WritableContent; + +/** + * Utility that can be used to dynamically compile and test Java source code. + * + * @author Phillip Webb + * @since 6.0 + * @see #forSystem() + */ +public final class TestCompiler { + + private final ClassLoader classLoader; + + private final JavaCompiler compiler; + + private final SourceFiles sourceFiles; + + private final ResourceFiles resourceFiles; + + + private TestCompiler(ClassLoader classLoader, JavaCompiler compiler, + SourceFiles sourceFiles, ResourceFiles resourceFiles) { + this.classLoader = classLoader; + this.compiler = compiler; + this.sourceFiles = sourceFiles; + this.resourceFiles = resourceFiles; + } + + + /** + * Return a new {@link TestCompiler} backed by the system java compiler. + * @return a new {@link TestCompiler} instance + */ + public static TestCompiler forSystem() { + return forCompiler(ToolProvider.getSystemJavaCompiler()); + } + + /** + * Return a new {@link TestCompiler} backed by the given + * {@link JavaCompiler}. + * @param javaCompiler the java compiler to use + * @return a new {@link TestCompiler} instance + */ + public static TestCompiler forCompiler(JavaCompiler javaCompiler) { + return new TestCompiler(null, javaCompiler, SourceFiles.none(), + ResourceFiles.none()); + } + + /** + * Return a new {@link TestCompiler} instance with addition source files. + * @param sourceFiles the additional source files + * @return a new {@link TestCompiler} instance + */ + public TestCompiler withSources(SourceFile... sourceFiles) { + return new TestCompiler(this.classLoader, this.compiler, + this.sourceFiles.and(sourceFiles), this.resourceFiles); + } + + /** + * Return a new {@link TestCompiler} instance with addition source files. + * @param sourceFiles the additional source files + * @return a new {@link TestCompiler} instance + */ + public TestCompiler withSources(SourceFiles sourceFiles) { + return new TestCompiler(this.classLoader, this.compiler, + this.sourceFiles.and(sourceFiles), this.resourceFiles); + } + + /** + * Return a new {@link TestCompiler} instance with addition resource files. + * @param resourceFiles the additional resource files + * @return a new {@link TestCompiler} instance + */ + public TestCompiler withResources(ResourceFile... resourceFiles) { + return new TestCompiler(this.classLoader, this.compiler, this.sourceFiles, + this.resourceFiles.and(resourceFiles)); + } + + /** + * Return a new {@link TestCompiler} instance with addition resource files. + * @param resourceFiles the additional resource files + * @return a new {@link TestCompiler} instance + */ + public TestCompiler withResources(ResourceFiles resourceFiles) { + return new TestCompiler(this.classLoader, this.compiler, this.sourceFiles, + this.resourceFiles.and(resourceFiles)); + } + + + /** + * Compile content from this instance along with the additional provided + * content. + * @param content the additional content to compile + * @param compiled a consumed used to further assert the compiled code + * @throws CompilationException if source cannot be compiled + */ + public void compile(WritableContent content, Consumer compiled) { + compile(SourceFile.of(content), compiled); + } + + /** + * Compile content from this instance along with the additional provided + * source file. + * @param sourceFile the additional source file to compile + * @param compiled a consumed used to further assert the compiled code + * @throws CompilationException if source cannot be compiled + */ + public void compile(SourceFile sourceFile, Consumer compiled) { + withSources(sourceFile).compile(compiled); + } + + /** + * Compile content from this instance along with the additional provided + * source files. + * @param sourceFiles the additional source files to compile + * @param compiled a consumed used to further assert the compiled code + * @throws CompilationException if source cannot be compiled + */ + public void compile(SourceFiles sourceFiles, Consumer compiled) { + withSources(sourceFiles).compile(compiled); + } + + /** + * Compile content from this instance along with the additional provided + * source and resource files. + * @param sourceFiles the additional source files to compile + * @param resourceFiles the additional resource files to include + * @param compiled a consumed used to further assert the compiled code + * @throws CompilationException if source cannot be compiled + */ + public void compile(SourceFiles sourceFiles, ResourceFiles resourceFiles, + Consumer compiled) { + withSources(sourceFiles).withResources(resourceFiles).compile(compiled); + } + + /** + * Compile content from this instance. + * @param compiled a consumed used to further assert the compiled code + * @throws CompilationException if source cannot be compiled + */ + public void compile(Consumer compiled) throws CompilationException { + DynamicClassLoader dynamicClassLoader = compile(); + ClassLoader previousClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(dynamicClassLoader); + compiled.accept(new Compiled(dynamicClassLoader, this.sourceFiles, + this.resourceFiles)); + } + finally { + Thread.currentThread().setContextClassLoader(previousClassLoader); + } + } + + private DynamicClassLoader compile() { + ClassLoader classLoader = (this.classLoader != null) ? this.classLoader + : Thread.currentThread().getContextClassLoader(); + List compilationUnits = this.sourceFiles.stream().map( + DynamicJavaFileObject::new).toList(); + StandardJavaFileManager standardFileManager = this.compiler.getStandardFileManager( + null, null, null); + DynamicJavaFileManager fileManager = new DynamicJavaFileManager( + standardFileManager, classLoader); + if (!this.sourceFiles.isEmpty()) { + Errors errors = new Errors(); + CompilationTask task = this.compiler.getTask(null, fileManager, errors, null, + null, compilationUnits); + boolean result = task.call(); + if (!result || errors.hasReportedErrors()) { + throw new CompilationException("Unable to compile source" + errors); + } + } + return new DynamicClassLoader(this.classLoader, this.sourceFiles, + this.resourceFiles, fileManager.getClassFiles()); + } + + + /** + * {@link DiagnosticListener} used to collect errors. + */ + static class Errors implements DiagnosticListener { + + private final StringBuilder message = new StringBuilder(); + + @Override + public void report(Diagnostic diagnostic) { + if (diagnostic.getKind() == Diagnostic.Kind.ERROR) { + this.message.append("\n"); + this.message.append(diagnostic.getMessage(Locale.getDefault())); + this.message.append(" "); + this.message.append(diagnostic.getSource().getName()); + this.message.append(" "); + this.message.append( + diagnostic.getLineNumber() + ":" + diagnostic.getColumnNumber()); + } + } + + boolean hasReportedErrors() { + return this.message.length() > 0; + } + + @Override + public String toString() { + return this.message.toString(); + } + + } + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/DynamicFile.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/DynamicFile.java new file mode 100644 index 00000000000..c0c40b88fe4 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/DynamicFile.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.generator.file; + +import java.io.IOException; +import java.util.Objects; + +import org.assertj.core.util.Strings; + +/** + * Abstract base class for dynamically generated files. + * + * @author Phillip Webb + * @since 6.0 + * @see SourceFile + * @see ResourceFile + */ +public abstract sealed class DynamicFile permits SourceFile,ResourceFile { + + + private final String path; + + private final String content; + + + protected DynamicFile(String path, String content) { + if (Strings.isNullOrEmpty(content)) { + throw new IllegalArgumentException("'path' must not to be empty"); + } + if (Strings.isNullOrEmpty(content)) { + throw new IllegalArgumentException("'content' must not to be empty"); + } + this.path = path; + this.content = content; + } + + + protected static String toString(WritableContent writableContent) { + if (writableContent == null) { + throw new IllegalArgumentException("'writableContent' must not to be empty"); + } + try { + StringBuilder stringBuilder = new StringBuilder(); + writableContent.writeTo(stringBuilder); + return stringBuilder.toString(); + } + catch (IOException ex) { + throw new IllegalStateException("Unable to read content", ex); + } + } + + /** + * Return the contents of the file. + * @return the file contents + */ + public String getContent() { + return this.content; + } + + /** + * Return the relative path of the file. + * @return the file path + */ + public String getPath() { + return this.path; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + DynamicFile other = (DynamicFile) obj; + return Objects.equals(this.path, other.path) + && Objects.equals(this.content, other.content); + } + + @Override + public int hashCode() { + return Objects.hash(this.path, this.content); + } + + @Override + public String toString() { + return this.path; + } + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/DynamicFileAssert.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/DynamicFileAssert.java new file mode 100644 index 00000000000..5afc8ba785c --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/DynamicFileAssert.java @@ -0,0 +1,54 @@ +/* + * 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.generator.file; + +import org.assertj.core.api.AbstractAssert; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Assertion methods for {@code DynamicFile} instances. + * + * @author Phillip Webb + * @since 6.0 + * @param the assertion type + * @param the file type + */ +public class DynamicFileAssert, F extends DynamicFile> + extends AbstractAssert { + + + DynamicFileAssert(F actual, Class selfType) { + super(actual, selfType); + } + + + public A contains(CharSequence... values) { + assertThat(this.actual.getContent()).contains(values); + return this.myself; + } + + public A isEqualTo(Object expected) { + if (expected instanceof DynamicFile) { + return super.isEqualTo(expected); + } + assertThat(this.actual.getContent()).isEqualTo( + expected != null ? expected.toString() : null); + return this.myself; + } + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/DynamicFiles.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/DynamicFiles.java new file mode 100644 index 00000000000..a0e26fc9ffb --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/DynamicFiles.java @@ -0,0 +1,114 @@ +/* + * 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.generator.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 javax.annotation.Nullable; + +/** + * Internal class used by {@link SourceFiles} and {@link ResourceFiles} to + * manage {@link DynamicFile} instances. + * + * @author Phillip Webb + * @since 6.0 + * @param the {@link DynamicFile} type + */ +final class DynamicFiles implements Iterable { + + + private static final DynamicFiles NONE = new DynamicFiles<>( + Collections.emptyMap()); + + + private final Map files; + + + private DynamicFiles(Map files) { + this.files = files; + } + + + @SuppressWarnings("unchecked") + static DynamicFiles none() { + return (DynamicFiles) NONE; + } + + DynamicFiles and(F[] files) { + Map merged = new LinkedHashMap<>(this.files); + Arrays.stream(files).forEach(file -> merged.put(file.getPath(), file)); + return new DynamicFiles<>(Collections.unmodifiableMap(merged)); + } + + DynamicFiles and(DynamicFiles files) { + Map merged = new LinkedHashMap<>(this.files); + merged.putAll(files.files); + return new DynamicFiles<>(Collections.unmodifiableMap(merged)); + } + + @Override + public Iterator iterator() { + return this.files.values().iterator(); + } + + Stream stream() { + return this.files.values().stream(); + } + + boolean isEmpty() { + return this.files.isEmpty(); + } + + @Nullable + F get(String path) { + return this.files.get(path); + } + + F getSingle() { + if (this.files.size() != 1) { + throw new IllegalStateException("No single file available"); + } + return this.files.values().iterator().next(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return this.files.equals(((DynamicFiles) 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/main/java/org/springframework/aot/test/generator/file/MethodAssert.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/MethodAssert.java new file mode 100644 index 00000000000..e08bb9f72ee --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/MethodAssert.java @@ -0,0 +1,63 @@ +/* + * 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.generator.file; + +import java.util.stream.Collectors; + +import com.thoughtworks.qdox.model.JavaMethod; +import com.thoughtworks.qdox.model.JavaParameter; +import org.assertj.core.api.AbstractAssert; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Assertion methods for {@code SourceFile} methods. + * + * @author Phillip Webb + * @since 6.0 + */ +public class MethodAssert extends AbstractAssert { + + + MethodAssert(JavaMethod actual) { + super(actual, MethodAssert.class); + as(describe(actual)); + } + + + private String describe(JavaMethod actual) { + return actual.getName() + "(" + + actual.getParameters().stream().map( + this::getFullyQualifiedName).collect(Collectors.joining(", ")) + + ")"; + } + + private String getFullyQualifiedName(JavaParameter parameter) { + return parameter.getType().getFullyQualifiedName(); + } + + public void withBody(String expected) { + assertThat(this.actual.getSourceCode()).as( + this.info.description()).isEqualToNormalizingWhitespace(expected); + } + + public void withBodyContaining(CharSequence... values) { + assertThat(this.actual.getSourceCode()).as(this.info.description()).contains( + values); + } + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/ResourceFile.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/ResourceFile.java new file mode 100644 index 00000000000..e44625d64d1 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/ResourceFile.java @@ -0,0 +1,72 @@ +/* + * 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.generator.file; + +import org.assertj.core.api.AssertProvider; + +/** + * {@link DynamicFile} that holds resource file content and provides + * {@link ResourceFileAssert} support. + * + * @author Phillip Webb + * @since 6.0 + */ +public final class ResourceFile extends DynamicFile + implements AssertProvider { + + + private ResourceFile(String path, String content) { + super(path, content); + } + + + /** + * Factory method to create a new {@link ResourceFile} from the given + * {@link CharSequence}. + * @param path the relative path of the file or {@code null} to have the + * path deduced + * @param charSequence a file containing the source contents + * @return a {@link ResourceFile} instance + */ + public static ResourceFile of(String path, CharSequence charSequence) { + return new ResourceFile(path, charSequence.toString()); + } + + /** + * Factory method to create a new {@link SourceFile} from the given + * {@link WritableContent}. + * @param path the relative path of the file or {@code null} to have the + * path deduced + * @param writableContent the content to write to the file + * @return a {@link SourceFile} instance + */ + public static ResourceFile of(String path, WritableContent writableContent) { + return new ResourceFile(path, toString(writableContent)); + } + + /** + * AssertJ {@code assertThat} support. + * @deprecated use {@code assertThat(sourceFile)} rather than calling this + * method directly. + */ + @Override + @Deprecated + public ResourceFileAssert assertThat() { + return new ResourceFileAssert(this); + } + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/ResourceFileAssert.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/ResourceFileAssert.java new file mode 100644 index 00000000000..60dcff164b2 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/ResourceFileAssert.java @@ -0,0 +1,34 @@ +/* + * 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.generator.file; + +/** + * Assertion methods for {@code ResourceFile} instances. + * + * @author Phillip Webb + * @since 6.0 + */ +public class ResourceFileAssert + extends DynamicFileAssert { + + + ResourceFileAssert(ResourceFile actual) { + super(actual, ResourceFileAssert.class); + } + + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/ResourceFiles.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/ResourceFiles.java new file mode 100644 index 00000000000..6e3b62829c8 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/ResourceFiles.java @@ -0,0 +1,144 @@ +/* + * 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.generator.file; + +import java.util.Iterator; +import java.util.stream.Stream; + +import javax.annotation.Nullable; + +/** + * An immutable collection of {@link ResourceFile} instances. + * + * @author Phillip Webb + * @since 6.0 + */ +public final class ResourceFiles implements Iterable { + + private static final ResourceFiles NONE = new ResourceFiles(DynamicFiles.none()); + + + private final DynamicFiles files; + + + private ResourceFiles(DynamicFiles files) { + this.files = files; + } + + + /** + * Return a {@link DynamicFiles} instance with no items. + * @return the empty instance + */ + public static ResourceFiles none() { + return NONE; + } + + /** + * Factory method that can be used to create a {@link ResourceFiles} + * instance containing the specified files. + * @param ResourceFiles the files to include + * @return a {@link ResourceFiles} instance + */ + public static ResourceFiles of(ResourceFile... ResourceFiles) { + return none().and(ResourceFiles); + } + + /** + * Return a new {@link ResourceFiles} instance that merges files from + * another array of {@link ResourceFile} instances. + * @param ResourceFiles the instances to merge + * @return a new {@link ResourceFiles} instance containing merged content + */ + public ResourceFiles and(ResourceFile... ResourceFiles) { + return new ResourceFiles(this.files.and(ResourceFiles)); + } + + /** + * Return a new {@link ResourceFiles} instance that merges files from + * another {@link ResourceFiles} instance. + * @param ResourceFiles the instance to merge + * @return a new {@link ResourceFiles} instance containing merged content + */ + public ResourceFiles and(ResourceFiles ResourceFiles) { + return new ResourceFiles(this.files.and(ResourceFiles.files)); + } + + @Override + public Iterator iterator() { + return this.files.iterator(); + } + + /** + * Stream the {@link ResourceFile} instances contained in this collection. + * @return a stream of file instances + */ + public Stream stream() { + return this.files.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 ResourceFile} with the given + * {@code DynamicFile#getPath() path}. + * @param path the path to find + * @return a {@link ResourceFile} instance or {@code null} + */ + @Nullable + public ResourceFile get(String path) { + return this.files.get(path); + } + + /** + * Return the single source file contained in the collection. + * @return the single file + * @throws IllegalStateException if the collection doesn't contain exactly + * one file + */ + public ResourceFile getSingle() throws IllegalStateException { + return this.files.getSingle(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return this.files.equals(((ResourceFiles) 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/main/java/org/springframework/aot/test/generator/file/SourceFile.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/SourceFile.java new file mode 100644 index 00000000000..5a7d4fe0dfa --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/SourceFile.java @@ -0,0 +1,161 @@ +/* + * 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.generator.file; + +import java.io.StringReader; + +import javax.annotation.Nullable; + +import com.thoughtworks.qdox.JavaProjectBuilder; +import com.thoughtworks.qdox.model.JavaClass; +import com.thoughtworks.qdox.model.JavaPackage; +import com.thoughtworks.qdox.model.JavaSource; +import org.assertj.core.api.AssertProvider; +import org.assertj.core.util.Strings; + +/** + * {@link DynamicFile} that holds Java source code and provides + * {@link SourceFileAssert} support. Usually created from an AOT generated type, + * for example: + * + *
+ * SourceFile.of(generatedFile::writeTo)
+ * 
+ * + * @author Phillip Webb + * @since 6.0 + */ +public final class SourceFile extends DynamicFile + implements AssertProvider { + + + private final JavaSource javaSource; + + + private SourceFile(String path, String content, JavaSource javaSource) { + super(path, content); + this.javaSource = javaSource; + } + + + /** + * Factory method to create a new {@link SourceFile} from the given + * {@link CharSequence}. + * @param charSequence a file containing the source contents + * @return a {@link SourceFile} instance + */ + public static SourceFile of(CharSequence charSequence) { + return of(null, appendable -> appendable.append(charSequence)); + } + + /** + * Factory method to create a new {@link SourceFile} from the given + * {@link CharSequence}. + * @param path the relative path of the file or {@code null} to have the + * path deduced + * @param charSequence a file containing the source contents + * @return a {@link SourceFile} instance + */ + public static SourceFile of(@Nullable String path, CharSequence charSequence) { + return of(path, appendable -> appendable.append(charSequence)); + } + + /** + * Factory method to create a new {@link SourceFile} from the given + * {@link WritableContent}. + * @param writableContent the content to write to the file + * @return a {@link SourceFile} instance + */ + public static SourceFile of(WritableContent writableContent) { + return of(null, writableContent); + } + + /** + * Factory method to create a new {@link SourceFile} from the given + * {@link WritableContent}. + * @param path the relative path of the file or {@code null} to have the + * path deduced + * @param writableContent the content to write to the file + * @return a {@link SourceFile} instance + */ + public static SourceFile of(@Nullable String path, WritableContent writableContent) { + String content = toString(writableContent); + if (Strings.isNullOrEmpty(content)) { + throw new IllegalStateException("WritableContent did not append any content"); + } + JavaSource javaSource = parse(content); + if (path == null || path.isEmpty()) { + path = deducePath(javaSource); + } + return new SourceFile(path, content, javaSource); + } + + private static JavaSource parse(String content) { + JavaProjectBuilder builder = new JavaProjectBuilder(); + try { + JavaSource javaSource = builder.addSource(new StringReader(content)); + if (javaSource.getClasses().size() != 1) { + throw new IllegalStateException("Source must define a single class"); + } + return javaSource; + } + catch (Exception ex) { + throw new IllegalStateException( + "Unable to parse source file content:\n\n" + content, ex); + } + } + + private static String deducePath(JavaSource javaSource) { + JavaPackage javaPackage = javaSource.getPackage(); + JavaClass javaClass = javaSource.getClasses().get(0); + String path = javaClass.getName() + ".java"; + if (javaPackage != null) { + path = javaPackage.getName().replace('.', '/') + "/" + path; + } + return path; + } + + JavaSource getJavaSource() { + return this.javaSource; + } + + /** + * Return the target class for this source file or {@code null}. The target + * class can be used if private lookup access is required. + * @return the target class + */ + @Nullable + public Class getTarget() { + return null; // Not yet supported + } + + public String getClassName() { + return this.javaSource.getClasses().get(0).getFullyQualifiedName(); + } + + /** + * AssertJ {@code assertThat} support. + * @deprecated use {@code assertThat(sourceFile)} rather than calling this + * method directly. + */ + @Override + @Deprecated + public SourceFileAssert assertThat() { + return new SourceFileAssert(this); + } + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/SourceFileAssert.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/SourceFileAssert.java new file mode 100644 index 00000000000..a8ae7be3806 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/SourceFileAssert.java @@ -0,0 +1,128 @@ +/* + * 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.generator.file; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import com.thoughtworks.qdox.model.JavaClass; +import com.thoughtworks.qdox.model.JavaMethod; +import com.thoughtworks.qdox.model.JavaParameter; +import com.thoughtworks.qdox.model.JavaType; +import org.assertj.core.error.BasicErrorMessageFactory; +import org.assertj.core.internal.Failures; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Assertion methods for {@code SourceFile} instances. + * + * @author Phillip Webb + * @since 6.0 + */ +public class SourceFileAssert extends DynamicFileAssert { + + + SourceFileAssert(SourceFile actual) { + super(actual, SourceFileAssert.class); + } + + + public SourceFileAssert implementsInterface(Class type) { + return implementsInterface((type != null) ? type.getName() : (String) null); + } + + public SourceFileAssert implementsInterface(String name) { + JavaClass javaClass = getJavaClass(); + assertThat(javaClass.getImplements()).as("implements").map( + JavaType::getFullyQualifiedName).contains(name); + return this; + } + + public MethodAssert hasMethodNamed(String name) { + JavaClass javaClass = getJavaClass(); + JavaMethod method = null; + for (JavaMethod candidate : javaClass.getMethods()) { + if (candidate.getName().equals(name)) { + if (method != null) { + throw Failures.instance().failure(this.info, + new BasicErrorMessageFactory(String.format( + "%nExpecting actual:%n %s%nto contain unique method:%n %s%n", + this.actual.getContent(), name))); + } + method = candidate; + } + } + if (method == null) { + throw Failures.instance().failure(this.info, + new BasicErrorMessageFactory(String.format( + "%nExpecting actual:%n %s%nto contain method:%n %s%n", + this.actual.getContent(), name))); + } + return new MethodAssert(method); + } + + public MethodAssert hasMethod(String name, Class... parameters) { + JavaClass javaClass = getJavaClass(); + JavaMethod method = null; + for (JavaMethod candidate : javaClass.getMethods()) { + if (candidate.getName().equals(name) + && hasParameters(candidate, parameters)) { + if (method != null) { + throw Failures.instance().failure(this.info, + new BasicErrorMessageFactory(String.format( + "%nExpecting actual:%n %s%nto contain unique method:%n %s%n", + this.actual.getContent(), name))); + } + method = candidate; + } + } + if (method == null) { + String methodDescription = getMethodDescription(name, parameters); + throw Failures.instance().failure(this.info, + new BasicErrorMessageFactory(String.format( + "%nExpecting actual:%n %s%nto contain method:%n %s%n", + this.actual.getContent(), methodDescription))); + } + return new MethodAssert(method); + } + + private boolean hasParameters(JavaMethod method, Class[] requiredParameters) { + List parameters = method.getParameters(); + if (parameters.size() != requiredParameters.length) { + return false; + } + for (int i = 0; i < requiredParameters.length; i++) { + if (!requiredParameters[i].getName().equals( + parameters.get(i).getFullyQualifiedName())) { + return false; + } + } + return true; + } + + private String getMethodDescription(String name, Class... parameters) { + return name + "(" + Arrays.stream(parameters).map(Class::getName).collect( + Collectors.joining(", ")) + ")"; + } + + private JavaClass getJavaClass() { + return this.actual.getJavaSource().getClasses().get(0); + } + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/SourceFiles.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/SourceFiles.java new file mode 100644 index 00000000000..e9eaf3e463b --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/SourceFiles.java @@ -0,0 +1,144 @@ +/* + * 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.generator.file; + +import java.util.Iterator; +import java.util.stream.Stream; + +import javax.annotation.Nullable; + +/** + * An immutable collection of {@link SourceFile} instances. + * + * @author Phillip Webb + * @since 6.0 + */ +public final class SourceFiles implements Iterable { + + private static final SourceFiles NONE = new SourceFiles(DynamicFiles.none()); + + + private final DynamicFiles files; + + + private SourceFiles(DynamicFiles files) { + this.files = files; + } + + + /** + * Return a {@link SourceFiles} instance with no items. + * @return the empty instance + */ + public static SourceFiles none() { + return NONE; + } + + /** + * Factory method that can be used to create a {@link SourceFiles} instance + * containing the specified files. + * @param sourceFiles the files to include + * @return a {@link SourceFiles} instance + */ + public static SourceFiles of(SourceFile... sourceFiles) { + return none().and(sourceFiles); + } + + /** + * Return a new {@link SourceFiles} instance that merges files from another + * array of {@link SourceFile} instances. + * @param sourceFiles the instances to merge + * @return a new {@link SourceFiles} instance containing merged content + */ + public SourceFiles and(SourceFile... sourceFiles) { + return new SourceFiles(this.files.and(sourceFiles)); + } + + /** + * Return a new {@link SourceFiles} instance that merges files from another + * {@link SourceFiles} instance. + * @param sourceFiles the instance to merge + * @return a new {@link SourceFiles} instance containing merged content + */ + public SourceFiles and(SourceFiles sourceFiles) { + return new SourceFiles(this.files.and(sourceFiles.files)); + } + + @Override + public Iterator iterator() { + return this.files.iterator(); + } + + /** + * Stream the {@link SourceFile} instances contained in this collection. + * @return a stream of file instances + */ + public Stream stream() { + return this.files.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 SourceFile} with the given + * {@code DynamicFile#getPath() path}. + * @param path the path to find + * @return a {@link SourceFile} instance or {@code null} + */ + @Nullable + public SourceFile get(String path) { + return this.files.get(path); + } + + /** + * Return the single source file contained in the collection. + * @return the single file + * @throws IllegalStateException if the collection doesn't contain exactly + * one file + */ + public SourceFile getSingle() throws IllegalStateException { + return this.files.getSingle(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return this.files.equals(((SourceFiles) 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/main/java/org/springframework/aot/test/generator/file/WritableContent.java b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/WritableContent.java new file mode 100644 index 00000000000..f0fb749e120 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generator/file/WritableContent.java @@ -0,0 +1,39 @@ +/* + * 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.generator.file; + +import java.io.IOException; + +/** + * Callback interface used to write file content. Designed to align with + * JavaPoet's {@code JavaFile.writeTo} method. + * + * @author Phillip Webb + * @since 6.0 + */ +@FunctionalInterface +public interface WritableContent { + + /** + * Callback method that should write the content to the given + * {@link Appendable}. + * @param out the {@link Appendable} used to receive the content + * @throws IOException on IO error + */ + void writeTo(Appendable out) throws IOException; + +} diff --git a/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/CompilationExceptionTests.java b/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/CompilationExceptionTests.java new file mode 100644 index 00000000000..119e3032ba3 --- /dev/null +++ b/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/CompilationExceptionTests.java @@ -0,0 +1,38 @@ +/* + * 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.generator.compile; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Tests for {@link CompilationException}. + * + * @author Phillip Webb + * @since 6.0 + */ +class CompilationExceptionTests { + + @Test + void getMessageReturnsMessage() { + CompilationException exception = new CompilationException("message"); + assertThat(exception).hasMessage("message"); + } + +} diff --git a/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/CompiledTests.java b/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/CompiledTests.java new file mode 100644 index 00000000000..f1437f7ecdc --- /dev/null +++ b/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/CompiledTests.java @@ -0,0 +1,194 @@ +/* + * 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.generator.compile; + +import java.util.List; +import java.util.concurrent.Callable; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.test.generator.file.ResourceFile; +import org.springframework.aot.test.generator.file.ResourceFiles; +import org.springframework.aot.test.generator.file.SourceFile; +import org.springframework.aot.test.generator.file.SourceFiles; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link Compiled}. + * + * @author Phillip Webb + * @since 6.0 + */ +class CompiledTests { + + private static final String HELLO_WORLD = """ + package com.example; + + public class HelloWorld implements java.util.function.Supplier { + + public String get() { + return "Hello World!"; + } + + } + """; + + private static final String HELLO_SPRING = """ + package com.example; + + public class HelloSpring implements java.util.function.Supplier { + + public String get() { + return "Hello Spring!"; // !! + } + + } + """; + + @Test + void getSourceFileWhenSingleReturnsSourceFile() { + SourceFile sourceFile = SourceFile.of(HELLO_WORLD); + TestCompiler.forSystem().compile(sourceFile, + compiled -> assertThat(compiled.getSourceFile()).isSameAs(sourceFile)); + } + + @Test + void getSourceFileWhenMultipleThrowsException() { + SourceFiles sourceFiles = SourceFiles.of(SourceFile.of(HELLO_WORLD), + SourceFile.of(HELLO_SPRING)); + TestCompiler.forSystem().compile(sourceFiles, + compiled -> assertThatIllegalStateException().isThrownBy( + compiled::getSourceFile)); + } + + @Test + void getSourceFileWhenNoneThrowsException() { + TestCompiler.forSystem().compile( + compiled -> assertThatIllegalStateException().isThrownBy( + compiled::getSourceFile)); + } + + @Test + void getSourceFilesReturnsSourceFiles() { + SourceFiles sourceFiles = SourceFiles.of(SourceFile.of(HELLO_WORLD), + SourceFile.of(HELLO_SPRING)); + TestCompiler.forSystem().compile(sourceFiles, + compiled -> assertThat(compiled.getSourceFiles()).isEqualTo(sourceFiles)); + } + + @Test + void getResourceFileWhenSingleReturnsSourceFile() { + ResourceFile resourceFile = ResourceFile.of("META-INF/myfile", "test"); + TestCompiler.forSystem().withResources(resourceFile).compile( + compiled -> assertThat(compiled.getResourceFile()).isSameAs( + resourceFile)); + } + + @Test + void getResourceFileWhenMultipleThrowsException() { + ResourceFiles resourceFiles = ResourceFiles.of( + ResourceFile.of("META-INF/myfile1", "test1"), + ResourceFile.of("META-INF/myfile2", "test2")); + TestCompiler.forSystem().withResources(resourceFiles).compile( + compiled -> assertThatIllegalStateException().isThrownBy( + () -> compiled.getResourceFile())); + } + + @Test + void getResourceFileWhenNoneThrowsException() { + TestCompiler.forSystem().compile( + compiled -> assertThatIllegalStateException().isThrownBy( + () -> compiled.getResourceFile())); + } + + @Test + void getResourceFilesReturnsResourceFiles() { + ResourceFiles resourceFiles = ResourceFiles.of( + ResourceFile.of("META-INF/myfile1", "test1"), + ResourceFile.of("META-INF/myfile2", "test2")); + TestCompiler.forSystem().withResources(resourceFiles).compile( + compiled -> assertThat(compiled.getResourceFiles()).isEqualTo( + resourceFiles)); + } + + @Test + void getInstanceWhenNoneMatchesThrowsException() { + TestCompiler.forSystem().compile(SourceFile.of(HELLO_WORLD), + compiled -> assertThatIllegalStateException().isThrownBy( + () -> compiled.getInstance(Callable.class))); + } + + @Test + void getInstanceWhenMultipleMatchesThrowsException() { + SourceFiles sourceFiles = SourceFiles.of(SourceFile.of(HELLO_WORLD), + SourceFile.of(HELLO_SPRING)); + TestCompiler.forSystem().compile(sourceFiles, + compiled -> assertThatIllegalStateException().isThrownBy( + () -> compiled.getInstance(Supplier.class))); + } + + @Test + void getInstanceWhenNoDefaultConstructorThrowsException() { + SourceFile sourceFile = SourceFile.of(""" + package com.example; + + public class HelloWorld implements java.util.function.Supplier { + + public HelloWorld(String name) { + } + + public String get() { + return "Hello World!"; + } + + } + """); + TestCompiler.forSystem().compile(sourceFile, + compiled -> assertThatIllegalStateException().isThrownBy( + () -> compiled.getInstance(Supplier.class))); + } + + @Test + void getInstanceReturnsInstance() { + TestCompiler.forSystem().compile(SourceFile.of(HELLO_WORLD), + compiled -> assertThat(compiled.getInstance(Supplier.class)).isNotNull()); + } + + @Test + void getInstanceByNameReturnsInstance() { + SourceFiles sourceFiles = SourceFiles.of(SourceFile.of(HELLO_WORLD), + SourceFile.of(HELLO_SPRING)); + TestCompiler.forSystem().compile(sourceFiles, + compiled -> assertThat(compiled.getInstance(Supplier.class, + "com.example.HelloWorld")).isNotNull()); + } + + @Test + void getAllCompiledClassesReturnsCompiledClasses() { + SourceFiles sourceFiles = SourceFiles.of(SourceFile.of(HELLO_WORLD), + SourceFile.of(HELLO_SPRING)); + TestCompiler.forSystem().compile(sourceFiles, compiled -> { + List> classes = compiled.getAllCompiledClasses(); + assertThat(classes.stream().map(Class::getName)).containsExactlyInAnyOrder( + "com.example.HelloWorld", "com.example.HelloSpring"); + }); + } + +} diff --git a/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/DynamicClassFileObjectTests.java b/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/DynamicClassFileObjectTests.java new file mode 100644 index 00000000000..66d95cf849c --- /dev/null +++ b/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/DynamicClassFileObjectTests.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.generator.compile; + +import java.io.ByteArrayInputStream; +import java.io.OutputStream; + +import javax.tools.JavaFileObject.Kind; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DynamicClassFileObject}. + * + * @author Phillip Webb + * @since 6.0 + */ +class DynamicClassFileObjectTests { + + @Test + void getUriReturnsGeneratedUriBasedOnClassName() { + DynamicClassFileObject fileObject = new DynamicClassFileObject( + "com.example.MyClass"); + assertThat(fileObject.toUri()).hasToString("class:///com/example/MyClass.class"); + } + + @Test + void getKindReturnsClass() { + DynamicClassFileObject fileObject = new DynamicClassFileObject( + "com.example.MyClass"); + assertThat(fileObject.getKind()).isEqualTo(Kind.CLASS); + } + + @Test + void openOutputStreamWritesToBytes() throws Exception { + DynamicClassFileObject fileObject = new DynamicClassFileObject( + "com.example.MyClass"); + try(OutputStream outputStream = fileObject.openOutputStream()) { + new ByteArrayInputStream("test".getBytes()).transferTo(outputStream); + } + assertThat(fileObject.getBytes()).isEqualTo("test".getBytes()); + } + +} diff --git a/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/DynamicJavaFileManagerTests.java b/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/DynamicJavaFileManagerTests.java new file mode 100644 index 00000000000..0a42b4fd584 --- /dev/null +++ b/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/DynamicJavaFileManagerTests.java @@ -0,0 +1,101 @@ +/* + * 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.generator.compile; + +import javax.tools.JavaFileManager; +import javax.tools.JavaFileManager.Location; +import javax.tools.JavaFileObject; +import javax.tools.JavaFileObject.Kind; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; + +/** + * Tests for {@link DynamicJavaFileManager}. + * + * @author Phillip Webb + * @since 6.0 + */ +class DynamicJavaFileManagerTests { + + @Mock + private JavaFileManager parentFileManager; + + @Mock + private Location location; + + private ClassLoader classLoader; + + private DynamicJavaFileManager fileManager; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + this.classLoader = new ClassLoader() { + }; + this.fileManager = new DynamicJavaFileManager(this.parentFileManager, + this.classLoader); + } + + @Test + void getClassLoaderReturnsClassLoader() { + assertThat(this.fileManager.getClassLoader(this.location)).isSameAs( + this.classLoader); + } + + @Test + void getJavaFileForOutputWhenClassKindReturnsDynamicClassFile() throws Exception { + JavaFileObject fileObject = this.fileManager.getJavaFileForOutput(this.location, + "com.example.MyClass", Kind.CLASS, null); + assertThat(fileObject).isInstanceOf(DynamicClassFileObject.class); + } + + @Test + void getJavaFileForOutputWhenClassKindAndAlreadySeenReturnsSameDynamicClassFile() + throws Exception { + JavaFileObject fileObject1 = this.fileManager.getJavaFileForOutput(this.location, + "com.example.MyClass", Kind.CLASS, null); + JavaFileObject fileObject2 = this.fileManager.getJavaFileForOutput(this.location, + "com.example.MyClass", Kind.CLASS, null); + assertThat(fileObject1).isSameAs(fileObject2); + } + + @Test + void getJavaFileForOutputWhenNotClassKindDelegatesToParentFileManager() + throws Exception { + this.fileManager.getJavaFileForOutput(this.location, "com.example.MyClass", + Kind.SOURCE, null); + then(this.parentFileManager).should().getJavaFileForOutput(this.location, + "com.example.MyClass", Kind.SOURCE, null); + } + + @Test + void getClassFilesReturnsClassFiles() throws Exception { + this.fileManager.getJavaFileForOutput(this.location, "com.example.MyClass1", + 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"); + } + +} diff --git a/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/DynamicJavaFileObjectTests.java b/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/DynamicJavaFileObjectTests.java new file mode 100644 index 00000000000..6cf19ca3e2c --- /dev/null +++ b/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/DynamicJavaFileObjectTests.java @@ -0,0 +1,47 @@ +/* + * 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.generator.compile; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.test.generator.file.SourceFile; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DynamicJavaFileObject}. + * + * @author Phillip Webb + * @since 6.0 + */ +class DynamicJavaFileObjectTests { + + private static final String CONTENT = "package com.example; public class Hello {}"; + + @Test + void getUriReturnsPath() { + DynamicJavaFileObject fileObject = new DynamicJavaFileObject(SourceFile.of(CONTENT)); + assertThat(fileObject.toUri()).hasToString("com/example/Hello.java"); + } + + @Test + void getCharContentReturnsContent() throws Exception { + DynamicJavaFileObject fileObject = new DynamicJavaFileObject(SourceFile.of(CONTENT)); + assertThat(fileObject.getCharContent(true)).isEqualTo(CONTENT); + } + +} diff --git a/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/TestCompilerTests.java b/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/TestCompilerTests.java new file mode 100644 index 00000000000..d63c1f86db6 --- /dev/null +++ b/spring-core-test/src/test/java/org/springframework/aot/test/generator/compile/TestCompilerTests.java @@ -0,0 +1,179 @@ +/* + * 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.generator.compile; + +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.test.generator.file.ResourceFile; +import org.springframework.aot.test.generator.file.ResourceFiles; +import org.springframework.aot.test.generator.file.SourceFile; +import org.springframework.aot.test.generator.file.SourceFiles; +import org.springframework.aot.test.generator.file.WritableContent; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link TestCompiler}. + * + * @author Phillip Webb + */ +class TestCompilerTests { + + private static final String HELLO_WORLD = """ + package com.example; + + import java.util.function.Supplier; + + @Deprecated + public class Hello implements Supplier { + + public String get() { + return "Hello World!"; + } + + } + """; + + private static final String HELLO_SPRING = """ + package com.example; + + import java.util.function.Supplier; + + public class Hello implements Supplier { + + public String get() { + return "Hello Spring!"; // !! + } + + } + """; + + private static final String HELLO_BAD = """ + package com.example; + + public class Hello implements Supplier { + + public String get() { + return "Missing Import!"; + } + + } + """; + + @Test + @SuppressWarnings("unchecked") + void compileWhenHasDifferentClassesWithSameClassNameCompilesBoth() { + TestCompiler.forSystem().withSources(SourceFile.of(HELLO_WORLD)).compile( + compiled -> { + Supplier supplier = compiled.getInstance(Supplier.class, + "com.example.Hello"); + assertThat(supplier.get()).isEqualTo("Hello World!"); + }); + TestCompiler.forSystem().withSources(SourceFile.of(HELLO_SPRING)).compile( + compiled -> { + Supplier supplier = compiled.getInstance(Supplier.class, + "com.example.Hello"); + assertThat(supplier.get()).isEqualTo("Hello Spring!"); + }); + } + + @Test + void compileAndGetSourceFile() { + TestCompiler.forSystem().withSources(SourceFile.of(HELLO_SPRING)).compile( + compiled -> assertThat(compiled.getSourceFile()).hasMethodNamed( + "get").withBodyContaining("// !!")); + } + + @Test + void compileWhenSourceHasCompileErrors() { + assertThatExceptionOfType(CompilationException.class).isThrownBy( + () -> TestCompiler.forSystem().withSources( + SourceFile.of(HELLO_BAD)).compile(compiled -> { + })); + } + + @Test + void withSourcesArrayAddsSource() { + SourceFile sourceFile = SourceFile.of(HELLO_WORLD); + TestCompiler.forSystem().withSources(sourceFile).compile( + this::assertSuppliesHelloWorld); + } + + @Test + void withSourcesAddsSource() { + SourceFiles sourceFiles = SourceFiles.of(SourceFile.of(HELLO_WORLD)); + TestCompiler.forSystem().withSources(sourceFiles).compile( + this::assertSuppliesHelloWorld); + } + + @Test + void withResourcesArrayAddsResource() { + ResourceFile resourceFile = ResourceFile.of("META-INF/myfile", "test"); + TestCompiler.forSystem().withResources(resourceFile).compile( + this::assertHasResource); + } + + @Test + void withResourcesAddsResource() { + ResourceFiles resourceFiles = ResourceFiles.of( + ResourceFile.of("META-INF/myfile", "test")); + TestCompiler.forSystem().withResources(resourceFiles).compile( + this::assertHasResource); + } + + @Test + void compileWithWritableContent() { + WritableContent content = appendable -> appendable.append(HELLO_WORLD); + TestCompiler.forSystem().compile(content, this::assertSuppliesHelloWorld); + } + + @Test + void compileWithSourceFile() { + SourceFile sourceFile = SourceFile.of(HELLO_WORLD); + TestCompiler.forSystem().compile(sourceFile, this::assertSuppliesHelloWorld); + } + + @Test + void compileWithSourceFiles() { + SourceFiles sourceFiles = SourceFiles.of(SourceFile.of(HELLO_WORLD)); + TestCompiler.forSystem().compile(sourceFiles, this::assertSuppliesHelloWorld); + } + + @Test + void compileWithSourceFilesAndResourceFiles() { + SourceFiles sourceFiles = SourceFiles.of(SourceFile.of(HELLO_WORLD)); + ResourceFiles resourceFiles = ResourceFiles.of( + ResourceFile.of("META-INF/myfile", "test")); + TestCompiler.forSystem().compile(sourceFiles, resourceFiles, compiled -> { + assertSuppliesHelloWorld(compiled); + assertHasResource(compiled); + }); + } + + private void assertSuppliesHelloWorld(Compiled compiled) { + assertThat(compiled.getInstance(Supplier.class).get()).isEqualTo("Hello World!"); + } + + private void assertHasResource(Compiled compiled) { + assertThat(compiled.getClassLoader().getResourceAsStream( + "META-INF/myfile")).hasContent("test"); + } + +} diff --git a/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/MethodAssertTests.java b/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/MethodAssertTests.java new file mode 100644 index 00000000000..ed2c469b6e2 --- /dev/null +++ b/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/MethodAssertTests.java @@ -0,0 +1,73 @@ +/* + * 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.generator.file; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link MethodAssert}. + * + * @author Phillip Webb + */ +class MethodAssertTests { + + private static final String SAMPLE = """ + package com.example; + + public class Sample { + + public void run() { + System.out.println("Hello World!"); + } + + } + """; + + private final SourceFile sourceFile = SourceFile.of(SAMPLE); + + @Test + void withBodyWhenMatches() { + assertThat(this.sourceFile).hasMethodNamed("run").withBody(""" + System.out.println("Hello World!");"""); + } + + @Test + void withBodyWhenDoesNotMatchThrowsException() { + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(this.sourceFile).hasMethodNamed("run").withBody(""" + System.out.println("Hello Spring!");""")).withMessageContaining( + "to be equal to"); + } + + @Test + void withBodyContainingWhenContainsAll() { + assertThat(this.sourceFile).hasMethodNamed("run").withBodyContaining("Hello", + "World!"); + } + + @Test + void withBodyWhenDoesNotContainOneThrowsException() { + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(this.sourceFile).hasMethodNamed( + "run").withBodyContaining("Hello", + "Spring!")).withMessageContaining("to contain"); + } + +} diff --git a/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/ResourceFileTests.java b/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/ResourceFileTests.java new file mode 100644 index 00000000000..e86115a93d9 --- /dev/null +++ b/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/ResourceFileTests.java @@ -0,0 +1,52 @@ +/* + * 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.generator.file; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ResourceFile}. + * + * @author Phillip Webb + * @since 6.0 + */ +class ResourceFileTests { + + @Test + void ofPathAndCharSequenceCreatesResource() { + ResourceFile file = ResourceFile.of("path", "test"); + assertThat(file.getPath()).isEqualTo("path"); + assertThat(file.getContent()).isEqualTo("test"); + } + + @Test + void ofPathAndWritableContentCreatesResource() { + ResourceFile file = ResourceFile.of("path", appendable -> appendable.append("test")); + assertThat(file.getPath()).isEqualTo("path"); + assertThat(file.getContent()).isEqualTo("test"); + } + + @Test + @SuppressWarnings("deprecation") + void assertThatReturnsResourceFileAssert() { + ResourceFile file = ResourceFile.of("path", "test"); + assertThat(file.assertThat()).isInstanceOf(ResourceFileAssert.class); + } + +} diff --git a/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/ResourceFilesTests.java b/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/ResourceFilesTests.java new file mode 100644 index 00000000000..901a9824c61 --- /dev/null +++ b/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/ResourceFilesTests.java @@ -0,0 +1,131 @@ +/* + * 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.generator.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.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatObject; + +class ReResourceFilesTests { + + private static final ResourceFile RESOURCE_FILE_1 = ResourceFile.of("path1", + "resource1"); + + private static final ResourceFile RESOURCE_FILE_2 = ResourceFile.of("path2", + "resource2"); + + @Test + void noneReturnsNone() { + ResourceFiles none = ResourceFiles.none(); + assertThat(none).isNotNull(); + assertThat(none.isEmpty()).isTrue(); + } + + @Test + void ofCreatesResourceFiles() { + ResourceFiles resourceFiles = ResourceFiles.of(RESOURCE_FILE_1, RESOURCE_FILE_2); + assertThat(resourceFiles).containsExactly(RESOURCE_FILE_1, RESOURCE_FILE_2); + } + + @Test + void andAddsResourceFiles() { + ResourceFiles resourceFiles = ResourceFiles.of(RESOURCE_FILE_1); + ResourceFiles added = resourceFiles.and(RESOURCE_FILE_2); + assertThat(resourceFiles).containsExactly(RESOURCE_FILE_1); + assertThat(added).containsExactly(RESOURCE_FILE_1, RESOURCE_FILE_2); + } + + @Test + void andResourceFilesAddsResourceFiles() { + ResourceFiles resourceFiles = ResourceFiles.of(RESOURCE_FILE_1); + ResourceFiles added = resourceFiles.and(ResourceFiles.of(RESOURCE_FILE_2)); + assertThat(resourceFiles).containsExactly(RESOURCE_FILE_1); + assertThat(added).containsExactly(RESOURCE_FILE_1, RESOURCE_FILE_2); + } + + @Test + void iteratorIteratesResourceFiles() { + ResourceFiles resourceFiles = ResourceFiles.of(RESOURCE_FILE_1, RESOURCE_FILE_2); + Iterator iterator = resourceFiles.iterator(); + assertThat(iterator.next()).isEqualTo(RESOURCE_FILE_1); + assertThat(iterator.next()).isEqualTo(RESOURCE_FILE_2); + assertThat(iterator.hasNext()).isFalse(); + } + + @Test + void streamStreamsResourceFiles() { + ResourceFiles resourceFiles = ResourceFiles.of(RESOURCE_FILE_1, RESOURCE_FILE_2); + assertThat(resourceFiles.stream()).containsExactly(RESOURCE_FILE_1, + RESOURCE_FILE_2); + } + + @Test + void isEmptyWhenEmptyReturnsTrue() { + ResourceFiles resourceFiles = ResourceFiles.of(); + assertThat(resourceFiles.isEmpty()).isTrue(); + } + + @Test + void isEmptyWhenNotEmptyReturnsFalse() { + ResourceFiles resourceFiles = ResourceFiles.of(RESOURCE_FILE_1); + assertThat(resourceFiles.isEmpty()).isFalse(); + } + + @Test + void getWhenHasFileReturnsFile() { + ResourceFiles resourceFiles = ResourceFiles.of(RESOURCE_FILE_1); + assertThat(resourceFiles.get("path1")).isNotNull(); + } + + @Test + void getWhenMissingFileReturnsNull() { + ResourceFiles resourceFiles = ResourceFiles.of(RESOURCE_FILE_2); + assertThatObject(resourceFiles.get("path1")).isNull(); + } + + @Test + void getSingleWhenHasNoFilesThrowsException() { + assertThatIllegalStateException().isThrownBy( + () -> ResourceFiles.none().getSingle()); + } + + @Test + void getSingleWhenHasMultipleFilesThrowsException() { + ResourceFiles resourceFiles = ResourceFiles.of(RESOURCE_FILE_1, RESOURCE_FILE_2); + assertThatIllegalStateException().isThrownBy(() -> resourceFiles.getSingle()); + } + + @Test + void getSingleWhenHasSingleFileReturnsFile() { + ResourceFiles resourceFiles = ResourceFiles.of(RESOURCE_FILE_1); + assertThat(resourceFiles.getSingle()).isEqualTo(RESOURCE_FILE_1); + } + + @Test + void equalsAndHashCode() { + ResourceFiles s1 = ResourceFiles.of(RESOURCE_FILE_1, RESOURCE_FILE_2); + ResourceFiles s2 = ResourceFiles.of(RESOURCE_FILE_1, RESOURCE_FILE_2); + ResourceFiles s3 = ResourceFiles.of(RESOURCE_FILE_1); + assertThat(s1.hashCode()).isEqualTo(s2.hashCode()); + assertThatObject(s1).isEqualTo(s2).isNotEqualTo(s3); + } + +} diff --git a/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/SourceFileAssertTests.java b/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/SourceFileAssertTests.java new file mode 100644 index 00000000000..0a2282bfe81 --- /dev/null +++ b/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/SourceFileAssertTests.java @@ -0,0 +1,128 @@ +/* + * 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.generator.file; + +import java.util.concurrent.Callable; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link SourceFileAssert}. + * + * @author Phillip Webb + */ +class SourceFileAssertTests { + + private static final String SAMPLE = """ + package com.example; + + import java.lang.Runnable; + + public class Sample implements Runnable { + + void run() { + run("Hello World!"); + } + + void run(String message) { + System.out.println(message); + } + + public static void main(String[] args) { + new Sample().run(); + } + } + """; + + private final SourceFile sourceFile = SourceFile.of(SAMPLE); + + @Test + void containsWhenContainsAll() { + assertThat(this.sourceFile).contains("Sample", "main"); + } + + @Test + void containsWhenMissingOneThrowsException() { + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(this.sourceFile).contains("Sample", + "missing")).withMessageContaining("to contain"); + } + + @Test + void isEqualToWhenEqual() { + assertThat(this.sourceFile).isEqualTo(SAMPLE); + } + + @Test + void isEqualToWhenNotEqualThrowsException() { + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(this.sourceFile).isEqualTo("no")).withMessageContaining( + "expected", "but was"); + } + + @Test + void implementsInterfaceWhenImplementsInterface() { + assertThat(this.sourceFile).implementsInterface(Runnable.class); + } + + @Test + void implementsInterfaceWhenDoesNotImplementInterfaceThrowsException() { + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(this.sourceFile).implementsInterface( + Callable.class)).withMessageContaining("to contain:"); + } + + @Test + void hasMethodNamedWhenHasName() { + MethodAssert methodAssert = assertThat(this.sourceFile).hasMethodNamed("main"); + assertThat(methodAssert).isNotNull(); + } + + @Test + void hasMethodNameWhenDoesNotHaveMethodThrowsException() { + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(this.sourceFile).hasMethodNamed( + "missing")).withMessageContaining("to contain method"); + } + + @Test + void hasMethodNameWhenHasMultipleMethodsWithNameThrowsException() { + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(this.sourceFile).hasMethodNamed( + "run")).withMessageContaining("to contain unique method"); + } + + @Test + void hasMethodWhenHasMethod() { + MethodAssert methodAssert = assertThat(this.sourceFile).hasMethod("run", + String.class); + assertThat(methodAssert).isNotNull(); + } + + @Test + void hasMethodWhenDoesNotHaveMethod() { + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(this.sourceFile).hasMethod("run", + Integer.class)).withMessageContaining( + "to contain").withMessageContaining( + "run(java.lang.Integer"); + } + +} diff --git a/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/SourceFileTests.java b/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/SourceFileTests.java new file mode 100644 index 00000000000..c22c62de2cc --- /dev/null +++ b/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/SourceFileTests.java @@ -0,0 +1,138 @@ +/* + * 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.generator.file; + +import java.io.IOException; + +import com.thoughtworks.qdox.model.JavaSource; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link SourceFile}. + * + * @author Phillip Webb + */ +class SourceFileTests { + + private static final String HELLO_WORLD = """ + package com.example.helloworld; + + public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello World!"); + } + } + """; + + @Test + void ofWhenContentIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> SourceFile.of((WritableContent) null)).withMessage( + "'writableContent' must not to be empty"); + } + + @Test + void ofWhenContentIsEmptyThrowsException() { + assertThatIllegalStateException().isThrownBy(() -> SourceFile.of("")).withMessage( + "WritableContent did not append any content"); + } + + @Test + void ofWhenSourceDefinesNoClassThrowsException() { + assertThatIllegalStateException().isThrownBy( + () -> SourceFile.of("package com.example;")).withMessageContaining( + "Unable to parse").havingCause().withMessage( + "Source must define a single class"); + } + + @Test + void ofWhenSourceDefinesMultipleClassesThrowsException() { + assertThatIllegalStateException().isThrownBy(() -> SourceFile.of( + "public class One {}\npublic class Two{}")).withMessageContaining( + "Unable to parse").havingCause().withMessage( + "Source must define a single class"); + } + + @Test + void ofWhenSourceCannotBeParsedThrowsException() { + assertThatIllegalStateException().isThrownBy( + () -> SourceFile.of("well this is broken {")).withMessageContaining( + "Unable to parse source file content"); + } + + @Test + void ofWithoutPathDeducesPath() { + SourceFile sourceFile = SourceFile.of(HELLO_WORLD); + assertThat(sourceFile.getPath()).isEqualTo( + "com/example/helloworld/HelloWorld.java"); + } + + @Test + void ofWithPathUsesPath() { + SourceFile sourceFile = SourceFile.of("com/example/DifferentPath.java", + HELLO_WORLD); + assertThat(sourceFile.getPath()).isEqualTo("com/example/DifferentPath.java"); + } + + @Test + void getContentReturnsContent() { + SourceFile sourceFile = SourceFile.of(HELLO_WORLD); + assertThat(sourceFile.getContent()).isEqualTo(HELLO_WORLD); + } + + @Test + void getJavaSourceReturnsJavaSource() { + SourceFile sourceFile = SourceFile.of(HELLO_WORLD); + assertThat(sourceFile.getJavaSource()).isInstanceOf(JavaSource.class); + } + + @Test + @SuppressWarnings("deprecation") + void assertThatReturnsAssert() { + SourceFile sourceFile = SourceFile.of(HELLO_WORLD); + assertThat(sourceFile.assertThat()).isInstanceOf(SourceFileAssert.class); + } + + @Test + void createFromJavaPoetStyleApi() { + JavaFile javaFile = new JavaFile(HELLO_WORLD); + SourceFile sourceFile = SourceFile.of(javaFile::writeTo); + assertThat(sourceFile.getContent()).isEqualTo(HELLO_WORLD); + } + + /** + * JavaPoet style API with a {@code writeTo} method. + */ + static class JavaFile { + + private final String content; + + JavaFile(String content) { + this.content = content; + } + + void writeTo(Appendable out) throws IOException { + out.append(this.content); + } + + } + +} diff --git a/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/SourceFilesTests.java b/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/SourceFilesTests.java new file mode 100644 index 00000000000..abe69ac2cdb --- /dev/null +++ b/spring-core-test/src/test/java/org/springframework/aot/test/generator/file/SourceFilesTests.java @@ -0,0 +1,135 @@ +/* + * 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.generator.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.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatObject; + +/** + * Tests for {@link SourceFiles}. + * + * @author Phillip Webb + */ +class SourceFilesTests { + + private static final SourceFile SOURCE_FILE_1 = SourceFile.of( + "public class Test1 {}"); + + private static final SourceFile SOURCE_FILE_2 = SourceFile.of( + "public class Test2 {}"); + + @Test + void noneReturnsNone() { + SourceFiles none = SourceFiles.none(); + assertThat(none).isNotNull(); + assertThat(none.isEmpty()).isTrue(); + } + + @Test + void ofCreatesSourceFiles() { + SourceFiles sourceFiles = SourceFiles.of(SOURCE_FILE_1, SOURCE_FILE_2); + assertThat(sourceFiles).containsExactly(SOURCE_FILE_1, SOURCE_FILE_2); + } + + @Test + void andAddsSourceFiles() { + SourceFiles sourceFiles = SourceFiles.of(SOURCE_FILE_1); + SourceFiles added = sourceFiles.and(SOURCE_FILE_2); + assertThat(sourceFiles).containsExactly(SOURCE_FILE_1); + assertThat(added).containsExactly(SOURCE_FILE_1, SOURCE_FILE_2); + } + + @Test + void andSourceFilesAddsSourceFiles() { + SourceFiles sourceFiles = SourceFiles.of(SOURCE_FILE_1); + SourceFiles added = sourceFiles.and(SourceFiles.of(SOURCE_FILE_2)); + assertThat(sourceFiles).containsExactly(SOURCE_FILE_1); + assertThat(added).containsExactly(SOURCE_FILE_1, SOURCE_FILE_2); + } + + @Test + void iteratorIteratesSourceFiles() { + SourceFiles sourceFiles = SourceFiles.of(SOURCE_FILE_1, SOURCE_FILE_2); + Iterator iterator = sourceFiles.iterator(); + assertThat(iterator.next()).isEqualTo(SOURCE_FILE_1); + assertThat(iterator.next()).isEqualTo(SOURCE_FILE_2); + assertThat(iterator.hasNext()).isFalse(); + } + + @Test + void streamStreamsSourceFiles() { + SourceFiles sourceFiles = SourceFiles.of(SOURCE_FILE_1, SOURCE_FILE_2); + assertThat(sourceFiles.stream()).containsExactly(SOURCE_FILE_1, SOURCE_FILE_2); + } + + @Test + void isEmptyWhenEmptyReturnsTrue() { + SourceFiles sourceFiles = SourceFiles.of(); + assertThat(sourceFiles.isEmpty()).isTrue(); + } + + @Test + void isEmptyWhenNotEmptyReturnsFalse() { + SourceFiles sourceFiles = SourceFiles.of(SOURCE_FILE_1); + assertThat(sourceFiles.isEmpty()).isFalse(); + } + + @Test + void getWhenHasFileReturnsFile() { + SourceFiles sourceFiles = SourceFiles.of(SOURCE_FILE_1); + assertThat(sourceFiles.get("Test1.java")).isNotNull(); + } + + @Test + void getWhenMissingFileReturnsNull() { + SourceFiles sourceFiles = SourceFiles.of(SOURCE_FILE_2); + assertThatObject(sourceFiles.get("Test1.java")).isNull(); + } + + @Test + void getSingleWhenHasNoFilesThrowsException() { + assertThatIllegalStateException().isThrownBy( + () -> SourceFiles.none().getSingle()); + } + + @Test + void getSingleWhenHasMultipleFilesThrowsException() { + SourceFiles sourceFiles = SourceFiles.of(SOURCE_FILE_1, SOURCE_FILE_2); + assertThatIllegalStateException().isThrownBy(() -> sourceFiles.getSingle()); + } + + @Test + void getSingleWhenHasSingleFileReturnsFile() { + SourceFiles sourceFiles = SourceFiles.of(SOURCE_FILE_1); + assertThat(sourceFiles.getSingle()).isEqualTo(SOURCE_FILE_1); + } + + @Test + void equalsAndHashCode() { + SourceFiles s1 = SourceFiles.of(SOURCE_FILE_1, SOURCE_FILE_2); + SourceFiles s2 = SourceFiles.of(SOURCE_FILE_1, SOURCE_FILE_2); + SourceFiles s3 = SourceFiles.of(SOURCE_FILE_1); + assertThat(s1.hashCode()).isEqualTo(s2.hashCode()); + assertThatObject(s1).isEqualTo(s2).isNotEqualTo(s3); + } + +}