diff options
Diffstat (limited to 'subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/DynamicTestLoader.java')
-rw-r--r-- | subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/DynamicTestLoader.java | 263 |
1 files changed, 263 insertions, 0 deletions
diff --git a/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/DynamicTestLoader.java b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/DynamicTestLoader.java new file mode 100644 index 00000000..001ee3a4 --- /dev/null +++ b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/DynamicTestLoader.java | |||
@@ -0,0 +1,263 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.generator.tests; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import com.google.inject.Provider; | ||
10 | import org.junit.jupiter.api.DynamicNode; | ||
11 | import org.junit.jupiter.api.function.Executable; | ||
12 | import org.opentest4j.TestAbortedException; | ||
13 | import tools.refinery.generator.ModelSemanticsFactory; | ||
14 | |||
15 | import java.io.IOException; | ||
16 | import java.net.URI; | ||
17 | import java.net.URISyntaxException; | ||
18 | import java.net.URL; | ||
19 | import java.nio.file.FileSystemNotFoundException; | ||
20 | import java.nio.file.FileSystems; | ||
21 | import java.nio.file.Files; | ||
22 | import java.nio.file.Path; | ||
23 | import java.util.ArrayList; | ||
24 | import java.util.Enumeration; | ||
25 | import java.util.List; | ||
26 | import java.util.stream.Stream; | ||
27 | |||
28 | import static org.junit.jupiter.api.DynamicContainer.dynamicContainer; | ||
29 | import static org.junit.jupiter.api.DynamicTest.dynamicTest; | ||
30 | |||
31 | public class DynamicTestLoader { | ||
32 | private static final String TEST_SUFFIX = ".problem"; | ||
33 | |||
34 | @Inject | ||
35 | private Provider<SemanticsTestLoader> testLoaderProvider; | ||
36 | |||
37 | private Provider<ModelSemanticsFactory> semanticsFactoryProvider; | ||
38 | |||
39 | @Inject | ||
40 | public void setSemanticsFactoryProvider(Provider<ModelSemanticsFactory> semanticsFactoryProvider) { | ||
41 | this.semanticsFactoryProvider = semanticsFactoryProvider; | ||
42 | } | ||
43 | |||
44 | public Stream<DynamicNode> allFromClasspath(Class<?> contextClass) { | ||
45 | var paths = getExtraPaths(contextClass); | ||
46 | if (paths.isEmpty()) { | ||
47 | throw new IllegalArgumentException("No file system paths found for class: " + contextClass); | ||
48 | } | ||
49 | var loader = getTestLoader(paths); | ||
50 | var nodes = new ArrayList<DynamicNode>(); | ||
51 | for (var path : paths) { | ||
52 | List<DynamicNode> childNodes; | ||
53 | try { | ||
54 | childNodes = nodesFromPath(loader, path); | ||
55 | } catch (IOException e) { | ||
56 | throw new IllegalArgumentException("Failed to enumerate path: " + path, e); | ||
57 | } | ||
58 | nodes.addAll(childNodes); | ||
59 | } | ||
60 | if (nodes.isEmpty()) { | ||
61 | throw new TestAbortedException("No tests found for class: " + contextClass); | ||
62 | } | ||
63 | return nodes.stream(); | ||
64 | } | ||
65 | |||
66 | private List<DynamicNode> nodesFromPath(SemanticsTestLoader loader, Path path) throws IOException { | ||
67 | var nodes = new ArrayList<DynamicNode>(); | ||
68 | try (var childList = Files.list(path)) { | ||
69 | var iterator = childList.iterator(); | ||
70 | while (iterator.hasNext()) { | ||
71 | var childPath = iterator.next(); | ||
72 | var fileName = childPath.getFileName().toString(); | ||
73 | var uri = childPath.toUri(); | ||
74 | try { | ||
75 | if (Files.isDirectory(childPath)) { | ||
76 | var childNodes = nodesFromPath(loader, childPath); | ||
77 | if (!childNodes.isEmpty()) { | ||
78 | nodes.add(dynamicContainer(fileName, uri, childNodes.stream())); | ||
79 | } | ||
80 | } else if (fileName.endsWith(TEST_SUFFIX)) { | ||
81 | SemanticsTest test = loader.loadFile(childPath.toFile()); | ||
82 | if (test != null) { | ||
83 | nodes.add(createDynamicNode(fileName, uri, test)); | ||
84 | } | ||
85 | } | ||
86 | } catch (IOException | RuntimeException e) { | ||
87 | nodes.add(createErrorNode(fileName, uri, e)); | ||
88 | } | ||
89 | } | ||
90 | } | ||
91 | return nodes; | ||
92 | } | ||
93 | |||
94 | public Stream<DynamicNode> fromClasspath(Class<?> contextClass, String name) { | ||
95 | var loader = getTestLoader(contextClass); | ||
96 | URL url; | ||
97 | URI uri; | ||
98 | try { | ||
99 | url = safeGetResource(contextClass, name); | ||
100 | uri = url.toURI(); | ||
101 | } catch (RuntimeException | URISyntaxException e) { | ||
102 | return Stream.of(createErrorNode(name, null, e)); | ||
103 | } | ||
104 | SemanticsTest test; | ||
105 | try { | ||
106 | test = loadFromUrl(loader, uri, url); | ||
107 | } catch (IOException | RuntimeException e) { | ||
108 | return Stream.of(createErrorNode(name, uri, e)); | ||
109 | } | ||
110 | return createDynamicNodes(uri, test); | ||
111 | } | ||
112 | |||
113 | public Stream<DynamicNode> fromClasspath(Class<?> contextClass, String... names) { | ||
114 | return fromClasspath(contextClass, Stream.of(names)); | ||
115 | } | ||
116 | |||
117 | public Stream<DynamicNode> fromClasspath(Class<?> contextClass, Stream<String> names) { | ||
118 | var loader = getTestLoader(contextClass); | ||
119 | return names.map(name -> nodeFromClasspath(loader, contextClass, name)); | ||
120 | } | ||
121 | |||
122 | private DynamicNode nodeFromClasspath(SemanticsTestLoader loader, Class<?> contextClass, String name) { | ||
123 | URL url; | ||
124 | URI uri; | ||
125 | try { | ||
126 | url = safeGetResource(contextClass, name); | ||
127 | uri = url.toURI(); | ||
128 | } catch (RuntimeException | URISyntaxException e) { | ||
129 | return createErrorNode(name, null, e); | ||
130 | } | ||
131 | SemanticsTest test; | ||
132 | try { | ||
133 | test = loadFromUrl(loader, uri, url); | ||
134 | } catch (IOException | RuntimeException e) { | ||
135 | return createErrorNode(name, uri, e); | ||
136 | } | ||
137 | return createDynamicNode(name, uri, test); | ||
138 | } | ||
139 | |||
140 | private static URL safeGetResource(Class<?> contextClass, String name) { | ||
141 | var url = contextClass.getResource(name); | ||
142 | if (url == null) { | ||
143 | throw new IllegalStateException("Test resource %s was not found.".formatted(name)); | ||
144 | } | ||
145 | return url; | ||
146 | } | ||
147 | |||
148 | private SemanticsTest loadFromUrl(SemanticsTestLoader testLoader, URI uri, URL url) throws IOException { | ||
149 | var eclipseUri = org.eclipse.emf.common.util.URI.createURI(uri.toString()); | ||
150 | try (var inputStream = url.openStream()) { | ||
151 | return testLoader.loadStream(inputStream, eclipseUri); | ||
152 | } | ||
153 | } | ||
154 | |||
155 | public Stream<DynamicNode> fromString(Class<?> contextClass, String problem) { | ||
156 | var testLoader = getTestLoader(contextClass); | ||
157 | SemanticsTest test; | ||
158 | try { | ||
159 | test = testLoader.loadString(problem); | ||
160 | } catch (RuntimeException e) { | ||
161 | return Stream.of(createErrorNode("<string>", null, e)); | ||
162 | } | ||
163 | return createDynamicNodes(null, test); | ||
164 | } | ||
165 | |||
166 | public Stream<DynamicNode> fromString(String problem) { | ||
167 | return fromString(null, problem); | ||
168 | } | ||
169 | |||
170 | private DynamicNode createDynamicNode(String name, URI uri, SemanticsTest test) { | ||
171 | var testCases = test.testCases(); | ||
172 | if (testCases.size() == 1 && testCases.getFirst().name() == null) { | ||
173 | var testCase = testCases.getFirst(); | ||
174 | return dynamicTest(name, uri, asExecutable(testCase)); | ||
175 | } | ||
176 | var children = createDynamicNodes(uri, test); | ||
177 | return dynamicContainer(name, uri, children); | ||
178 | } | ||
179 | |||
180 | private Stream<DynamicNode> createDynamicNodes(URI uri, SemanticsTest test) { | ||
181 | var testCases = test.testCases(); | ||
182 | var children = new ArrayList<DynamicNode>(); | ||
183 | int testCaseCount = testCases.size(); | ||
184 | for (int i = 0; i < testCaseCount; i++) { | ||
185 | var testCase = testCases.get(i); | ||
186 | var testCaseName = testCase.name(); | ||
187 | if (testCaseName == null) { | ||
188 | testCaseName = "[%d]".formatted(i + 1); | ||
189 | } | ||
190 | children.add(dynamicTest(testCaseName, uri, asExecutable(testCase))); | ||
191 | } | ||
192 | return children.stream(); | ||
193 | } | ||
194 | |||
195 | private Executable asExecutable(SemanticsTestCase testCase) { | ||
196 | return () -> testCase.execute(semanticsFactoryProvider.get()); | ||
197 | } | ||
198 | |||
199 | private DynamicNode createErrorNode(String name, URI uri, Throwable cause) { | ||
200 | var messageBuilder = new StringBuilder(); | ||
201 | messageBuilder.append("Error while reading test '").append(name).append("'"); | ||
202 | if (uri != null) { | ||
203 | messageBuilder.append(" from ").append(uri); | ||
204 | } | ||
205 | if (cause != null) { | ||
206 | messageBuilder.append(": ").append(cause.getMessage()); | ||
207 | } | ||
208 | var message = messageBuilder.toString(); | ||
209 | return dynamicTest(name, uri, () -> { | ||
210 | throw new RuntimeException(message, cause); | ||
211 | }); | ||
212 | } | ||
213 | |||
214 | private SemanticsTestLoader getTestLoader(Class<?> contextClass) { | ||
215 | var extraPaths = getExtraPaths(contextClass); | ||
216 | return getTestLoader(extraPaths); | ||
217 | } | ||
218 | |||
219 | private SemanticsTestLoader getTestLoader(List<Path> extraPaths) { | ||
220 | var loader = testLoaderProvider.get(); | ||
221 | extraPaths.forEach(loader::extraPath); | ||
222 | return loader; | ||
223 | } | ||
224 | |||
225 | private List<Path> getExtraPaths(Class<?> contextClass) { | ||
226 | if (contextClass == null) { | ||
227 | return List.of(); | ||
228 | } | ||
229 | var resourceName = contextClass.getPackageName().replace('.', '/'); | ||
230 | Enumeration<URL> resources; | ||
231 | try { | ||
232 | resources = contextClass.getClassLoader().getResources(resourceName); | ||
233 | } catch (IOException e) { | ||
234 | // Failed to find any resources. | ||
235 | return List.of(); | ||
236 | } | ||
237 | var directories = new ArrayList<Path>(); | ||
238 | while (resources.hasMoreElements()) { | ||
239 | var url = resources.nextElement(); | ||
240 | var path = getPath(url); | ||
241 | if (path != null && path.getFileSystem() == FileSystems.getDefault()) { | ||
242 | directories.add(path); | ||
243 | } | ||
244 | } | ||
245 | return directories; | ||
246 | } | ||
247 | |||
248 | private static Path getPath(URL url) { | ||
249 | URI uri; | ||
250 | try { | ||
251 | uri = url.toURI(); | ||
252 | } catch (URISyntaxException e) { | ||
253 | return null; | ||
254 | } | ||
255 | Path path; | ||
256 | try { | ||
257 | path = Path.of(uri); | ||
258 | } catch (FileSystemNotFoundException e) { | ||
259 | return null; | ||
260 | } | ||
261 | return path.toAbsolutePath(); | ||
262 | } | ||
263 | } | ||