Skip to content

Commit 653dc59

Browse files
philwebbsnicoll
authored andcommitted
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
1 parent b5695b9 commit 653dc59

34 files changed

+3163
-1
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ configure(allprojects) { project ->
7474
dependency "com.google.code.gson:gson:2.8.9"
7575
dependency "com.google.protobuf:protobuf-java-util:3.19.3"
7676
dependency "com.googlecode.protobuf-java-format:protobuf-java-format:1.4"
77+
dependency "com.thoughtworks.qdox:qdox:2.0.1"
7778
dependency("com.thoughtworks.xstream:xstream:1.4.18") {
7879
exclude group: "xpp3", name: "xpp3_min"
7980
exclude group: "xmlpull", name: "xmlpull"

framework-bom/framework-bom.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ group = "org.springframework"
77

88
dependencies {
99
constraints {
10-
parent.moduleProjects.sort { "$it.name" }.each {
10+
parent.moduleProjects.findAll{ it.name != 'spring-core-test' }.sort{ "$it.name" }.each {
1111
api it
1212
}
1313
}

settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ include "spring-context"
1818
include "spring-context-indexer"
1919
include "spring-context-support"
2020
include "spring-core"
21+
include "spring-core-test"
2122
include "spring-expression"
2223
include "spring-instrument"
2324
include "spring-jcl"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
2+
3+
description = "Spring Core Test"
4+
5+
dependencies {
6+
api(project(":spring-core"))
7+
api("org.assertj:assertj-core")
8+
api("com.thoughtworks.qdox:qdox")
9+
}
10+
11+
tasks.withType(PublishToMavenRepository).configureEach {
12+
it.enabled = false
13+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.aot.test.generator.compile;
18+
19+
/**
20+
* Exception thrown when code cannot compile.
21+
*
22+
* @author Phillip Webb
23+
* @since 6.0
24+
*/
25+
@SuppressWarnings("serial")
26+
public class CompilationException extends RuntimeException {
27+
28+
CompilationException(String message) {
29+
super(message);
30+
}
31+
32+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.aot.test.generator.compile;
18+
19+
import java.lang.reflect.Constructor;
20+
import java.util.ArrayList;
21+
import java.util.Collections;
22+
import java.util.List;
23+
24+
import org.springframework.aot.test.generator.file.ResourceFile;
25+
import org.springframework.aot.test.generator.file.ResourceFiles;
26+
import org.springframework.aot.test.generator.file.SourceFile;
27+
import org.springframework.aot.test.generator.file.SourceFiles;
28+
import org.springframework.util.Assert;
29+
30+
/**
31+
* Fully compiled results provided from a {@link TestCompiler}.
32+
*
33+
* @author Phillip Webb
34+
* @since 6.0
35+
*/
36+
public class Compiled {
37+
38+
39+
private final ClassLoader classLoader;
40+
41+
private final SourceFiles sourceFiles;
42+
43+
private final ResourceFiles resourceFiles;
44+
45+
private List<Class<?>> compiledClasses;
46+
47+
48+
Compiled(ClassLoader classLoader, SourceFiles sourceFiles,
49+
ResourceFiles resourceFiles) {
50+
this.classLoader = classLoader;
51+
this.sourceFiles = sourceFiles;
52+
this.resourceFiles = resourceFiles;
53+
}
54+
55+
56+
/**
57+
* Return the classloader containing the compiled content and access to the
58+
* resources.
59+
* @return the classLoader
60+
*/
61+
public ClassLoader getClassLoader() {
62+
return this.classLoader;
63+
}
64+
65+
/**
66+
* Return the single source file that was compiled.
67+
* @return the single source file
68+
* @throws IllegalStateException if the compiler wasn't passed exactly one
69+
* file
70+
*/
71+
public SourceFile getSourceFile() {
72+
return this.sourceFiles.getSingle();
73+
}
74+
75+
/**
76+
* Return all source files that were compiled.
77+
* @return the source files used by the compiler
78+
*/
79+
public SourceFiles getSourceFiles() {
80+
return this.sourceFiles;
81+
}
82+
83+
/**
84+
* Return the single resource file that was used when compiled.
85+
* @return the single resource file
86+
* @throws IllegalStateException if the compiler wasn't passed exactly one
87+
* file
88+
*/
89+
public ResourceFile getResourceFile() {
90+
return this.resourceFiles.getSingle();
91+
}
92+
93+
/**
94+
* Return all resource files that were compiled.
95+
* @return the resource files used by the compiler
96+
*/
97+
public ResourceFiles getResourceFiles() {
98+
return this.resourceFiles;
99+
}
100+
101+
/**
102+
* Return a new instance of a compiled class of the given type. There must
103+
* be only a single instance and it must have a default constructor.
104+
* @param <T> the required type
105+
* @param type the required type
106+
* @return an instance of type created from the compiled classes
107+
* @throws IllegalStateException if no instance can be found or instantiated
108+
*/
109+
public <T> T getInstance(Class<T> type) {
110+
List<Class<?>> matching = getAllCompiledClasses().stream().filter(
111+
candidate -> type.isAssignableFrom(candidate)).toList();
112+
Assert.state(!matching.isEmpty(), () -> "No instance found of type " + type.getName());
113+
Assert.state(matching.size() == 1, () -> "Multiple instances found of type " + type.getName());
114+
return newInstance(matching.get(0));
115+
}
116+
117+
/**
118+
* Return an instance of a compiled class identified by its class name. The
119+
* class must have a default constructor.
120+
* @param <T> the type to return
121+
* @param type the type to return
122+
* @param className the class name to load
123+
* @return an instance of the class
124+
* @throws IllegalStateException if no instance can be found or instantiated
125+
*/
126+
public <T> T getInstance(Class<T> type, String className) {
127+
Class<?> loaded = loadClass(className);
128+
return newInstance(loaded);
129+
}
130+
131+
/**
132+
* Return all compiled classes.
133+
* @return a list of all compiled classes
134+
*/
135+
public List<Class<?>> getAllCompiledClasses() {
136+
List<Class<?>> compiledClasses = this.compiledClasses;
137+
if (compiledClasses == null) {
138+
compiledClasses = new ArrayList<>();
139+
this.sourceFiles.stream().map(this::loadClass).forEach(compiledClasses::add);
140+
this.compiledClasses = Collections.unmodifiableList(compiledClasses);
141+
}
142+
return compiledClasses;
143+
}
144+
145+
@SuppressWarnings("unchecked")
146+
private <T> T newInstance(Class<?> loaded) {
147+
try {
148+
Constructor<?> constructor = loaded.getDeclaredConstructor();
149+
return (T) constructor.newInstance();
150+
}
151+
catch (Exception ex) {
152+
throw new IllegalStateException(ex);
153+
}
154+
}
155+
156+
private Class<?> loadClass(SourceFile sourceFile) {
157+
return loadClass(sourceFile.getClassName());
158+
}
159+
160+
private Class<?> loadClass(String className) {
161+
try {
162+
return this.classLoader.loadClass(className);
163+
}
164+
catch (ClassNotFoundException ex) {
165+
throw new IllegalStateException(ex);
166+
}
167+
}
168+
169+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.aot.test.generator.compile;
18+
19+
import java.io.ByteArrayOutputStream;
20+
import java.io.IOException;
21+
import java.io.OutputStream;
22+
import java.net.URI;
23+
24+
import javax.tools.JavaFileObject;
25+
import javax.tools.SimpleJavaFileObject;
26+
27+
/**
28+
* In-memory {@link JavaFileObject} used to hold class bytecode.
29+
*
30+
* @author Phillip Webb
31+
* @since 6.0
32+
*/
33+
class DynamicClassFileObject extends SimpleJavaFileObject {
34+
35+
private volatile byte[] bytes;
36+
37+
38+
DynamicClassFileObject(String className) {
39+
super(URI.create("class:///" + className.replace('.', '/') + ".class"),
40+
Kind.CLASS);
41+
}
42+
43+
44+
@Override
45+
public OutputStream openOutputStream() throws IOException {
46+
return new JavaClassOutputStream();
47+
}
48+
49+
byte[] getBytes() {
50+
return this.bytes;
51+
}
52+
53+
54+
class JavaClassOutputStream extends ByteArrayOutputStream {
55+
56+
@Override
57+
public void close() throws IOException {
58+
DynamicClassFileObject.this.bytes = toByteArray();
59+
}
60+
61+
}
62+
63+
}

0 commit comments

Comments
 (0)