1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 package gov.nist.secauto.metaschema.model.testing;
28
29 import static org.junit.jupiter.api.Assertions.assertEquals;
30
31 import gov.nist.secauto.metaschema.core.model.IModule;
32 import gov.nist.secauto.metaschema.core.model.MetaschemaException;
33 import gov.nist.secauto.metaschema.core.model.validation.IContentValidator;
34 import gov.nist.secauto.metaschema.core.model.validation.IValidationFinding;
35 import gov.nist.secauto.metaschema.core.model.validation.IValidationResult;
36 import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator.JsonValidationFinding;
37 import gov.nist.secauto.metaschema.core.model.xml.ModuleLoader;
38 import gov.nist.secauto.metaschema.core.util.ObjectUtils;
39 import gov.nist.secauto.metaschema.databind.DefaultBindingContext;
40 import gov.nist.secauto.metaschema.databind.IBindingContext;
41 import gov.nist.secauto.metaschema.databind.io.Format;
42 import gov.nist.secauto.metaschema.databind.io.ISerializer;
43 import gov.nist.secauto.metaschema.model.testing.xml.xmlbeans.ContentCaseType;
44 import gov.nist.secauto.metaschema.model.testing.xml.xmlbeans.GenerateSchemaDocument.GenerateSchema;
45 import gov.nist.secauto.metaschema.model.testing.xml.xmlbeans.MetaschemaDocument;
46 import gov.nist.secauto.metaschema.model.testing.xml.xmlbeans.TestCollectionDocument.TestCollection;
47 import gov.nist.secauto.metaschema.model.testing.xml.xmlbeans.TestScenarioDocument.TestScenario;
48 import gov.nist.secauto.metaschema.model.testing.xml.xmlbeans.TestSuiteDocument;
49
50 import org.apache.logging.log4j.LogBuilder;
51 import org.apache.logging.log4j.LogManager;
52 import org.apache.logging.log4j.Logger;
53 import org.apache.xmlbeans.XmlException;
54 import org.apache.xmlbeans.XmlOptions;
55 import org.junit.jupiter.api.DynamicContainer;
56 import org.junit.jupiter.api.DynamicNode;
57 import org.junit.jupiter.api.DynamicTest;
58 import org.junit.platform.commons.JUnitException;
59
60 import java.io.IOException;
61 import java.io.Writer;
62 import java.net.URI;
63 import java.net.URISyntaxException;
64 import java.net.URL;
65 import java.nio.charset.StandardCharsets;
66 import java.nio.file.FileVisitResult;
67 import java.nio.file.Files;
68 import java.nio.file.OpenOption;
69 import java.nio.file.Path;
70 import java.nio.file.SimpleFileVisitor;
71 import java.nio.file.StandardOpenOption;
72 import java.nio.file.attribute.BasicFileAttributes;
73 import java.util.concurrent.ExecutionException;
74 import java.util.concurrent.ExecutorService;
75 import java.util.concurrent.Executors;
76 import java.util.concurrent.Future;
77 import java.util.function.BiFunction;
78 import java.util.function.Function;
79 import java.util.function.Supplier;
80 import java.util.stream.Stream;
81
82 import edu.umd.cs.findbugs.annotations.NonNull;
83 import edu.umd.cs.findbugs.annotations.Nullable;
84
85 public abstract class AbstractTestSuite {
86 private static final Logger LOGGER = LogManager.getLogger(AbstractTestSuite.class);
87 private static final ModuleLoader LOADER = new ModuleLoader();
88
89 private static final boolean DELETE_RESULTS_ON_EXIT = false;
90
91 static {
92 LOADER.allowEntityResolution();
93 }
94
95 @NonNull
96 protected abstract Format getRequiredContentFormat();
97
98 @NonNull
99 protected abstract URI getTestSuiteURI();
100
101 @NonNull
102 protected abstract Path getGenerationPath();
103
104 @NonNull
105 protected abstract BiFunction<IModule, Writer, Void> getGeneratorSupplier();
106
107 @Nullable
108 protected abstract Supplier<? extends IContentValidator> getSchemaValidatorSupplier();
109
110 @NonNull
111 protected abstract Function<Path, ? extends IContentValidator> getContentValidatorSupplier();
112
113 protected Stream<DynamicNode> testFactory() {
114 try {
115 return generateTests();
116 } catch (XmlException | IOException ex) {
117 throw new JUnitException("Unable to generate tests", ex);
118 }
119 }
120
121 private Stream<DynamicNode> generateTests() throws XmlException, IOException {
122 XmlOptions options = new XmlOptions();
123 options.setBaseURI(null);
124 options.setLoadLineNumbers();
125
126 Path generationPath = getGenerationPath();
127 if (Files.exists(generationPath)) {
128 if (!Files.isDirectory(generationPath)) {
129 throw new JUnitException(String.format("Generation path '%s' exists and is not a directory", generationPath));
130 }
131 } else {
132 Files.createDirectories(generationPath);
133 }
134
135 URI testSuiteUri = getTestSuiteURI();
136 URL testSuiteUrl = testSuiteUri.toURL();
137 TestSuiteDocument directive = TestSuiteDocument.Factory.parse(testSuiteUrl, options);
138 return directive.getTestSuite().getTestCollectionList().stream()
139 .flatMap(
140 collection -> Stream.of(generateCollection(ObjectUtils.notNull(collection), testSuiteUri, generationPath)));
141 }
142
143 protected void deleteCollectionOnExit(Path path) {
144 if (path != null) {
145 Runtime.getRuntime().addShutdownHook(new Thread(
146 new Runnable() {
147 @Override
148 public void run() {
149 try {
150 Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
151 @Override
152 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
153 Files.delete(file);
154 return FileVisitResult.CONTINUE;
155 }
156
157 @Override
158 public FileVisitResult postVisitDirectory(Path dir, IOException ex) throws IOException {
159 if (ex == null) {
160 Files.delete(dir);
161 return FileVisitResult.CONTINUE;
162 }
163
164 throw ex;
165 }
166 });
167 } catch (IOException ex) {
168 throw new IllegalStateException("Failed to delete collection: " + path, ex);
169 }
170 }
171 }));
172 }
173 }
174
175 private DynamicContainer generateCollection(@NonNull TestCollection collection, @NonNull URI testSuiteUri,
176 @NonNull Path generationPath) {
177 URI collectionUri = testSuiteUri.resolve(collection.getLocation());
178 assert collectionUri != null;
179
180 LOGGER.atInfo().log("Collection: " + collectionUri);
181 Path collectionGenerationPath;
182 try {
183 collectionGenerationPath = ObjectUtils.notNull(Files.createTempDirectory(generationPath, "collection-"));
184 assert collectionGenerationPath != null;
185 if (DELETE_RESULTS_ON_EXIT) {
186 deleteCollectionOnExit(collectionGenerationPath);
187 }
188 } catch (IOException ex) {
189 throw new JUnitException("Unable to create collection temp directory", ex);
190 }
191
192 return DynamicContainer.dynamicContainer(
193 collection.getName(),
194 testSuiteUri,
195 collection.getTestScenarioList().stream()
196 .flatMap(scenario -> {
197 assert scenario != null;
198 return Stream.of(generateScenario(scenario, collectionUri, collectionGenerationPath));
199 })
200 .sequential());
201 }
202
203 protected void produceSchema(@NonNull IModule module, @NonNull Path schemaPath) throws IOException {
204 produceSchema(module, schemaPath, getGeneratorSupplier());
205 }
206
207 protected void produceSchema(@NonNull IModule module, @NonNull Path schemaPath,
208 @NonNull BiFunction<IModule, Writer, Void> schemaProducer) throws IOException {
209 Path parentDir = schemaPath.getParent();
210 if (parentDir != null && !Files.exists(parentDir)) {
211 Files.createDirectories(parentDir);
212 }
213
214 try (Writer writer = Files.newBufferedWriter(
215 schemaPath,
216 StandardCharsets.UTF_8,
217 getWriteOpenOptions())) {
218 schemaProducer.apply(module, writer);
219 }
220 LOGGER.atInfo().log("Produced schema '{}' for module '{}'", schemaPath, module.getLocation());
221 }
222
223 protected OpenOption[] getWriteOpenOptions() {
224 return new OpenOption[] {
225 StandardOpenOption.CREATE,
226 StandardOpenOption.WRITE,
227 StandardOpenOption.TRUNCATE_EXISTING
228 };
229 }
230
231 private DynamicContainer generateScenario(@NonNull TestScenario scenario, @NonNull URI collectionUri,
232 @NonNull Path collectionGenerationPath) {
233 Path scenarioGenerationPath;
234 try {
235 scenarioGenerationPath = Files.createTempDirectory(collectionGenerationPath, "scenario-");
236 } catch (IOException ex) {
237 throw new JUnitException("Unable to create scenario temp directory", ex);
238 }
239
240 try {
241
242 Files.createDirectories(scenarioGenerationPath);
243 } catch (IOException ex) {
244 throw new JUnitException("Unable to create test directories for path: " + scenarioGenerationPath, ex);
245 }
246
247 GenerateSchema generateSchema = scenario.getGenerateSchema();
248 MetaschemaDocument.Metaschema metaschemaDirective = generateSchema.getMetaschema();
249 URI metaschemaUri = collectionUri.resolve(metaschemaDirective.getLocation());
250
251 ExecutorService executor = Executors.newSingleThreadExecutor();
252 Future<IModule> loadModuleFuture = executor.submit(() -> {
253 IModule module;
254 try {
255 module = LOADER.load(ObjectUtils.notNull(metaschemaUri.toURL()));
256 } catch (IOException | MetaschemaException ex) {
257 throw new JUnitException("Unable to generate schema for Module: " + metaschemaUri, ex);
258 }
259 return module;
260 });
261
262 Future<Path> generateSchemaFuture = executor.submit(() -> {
263 Path schemaPath;
264
265 try {
266 schemaPath = Files.createTempFile(scenarioGenerationPath, "", "-schema");
267 } catch (IOException ex) {
268 throw new JUnitException("Unable to create schema temp file", ex);
269 }
270 IModule module = loadModuleFuture.get();
271 produceSchema(ObjectUtils.notNull(module), ObjectUtils.notNull(schemaPath));
272 return schemaPath;
273 });
274
275 Future<IBindingContext> dynamicBindingContextFuture = executor.submit(() -> {
276 IModule module = loadModuleFuture.get();
277 IBindingContext context;
278 try {
279 context = new DefaultBindingContext();
280 context.registerModule(
281 ObjectUtils.notNull(module),
282 ObjectUtils.notNull(scenarioGenerationPath));
283 } catch (Exception ex) {
284 throw new JUnitException("Unable to generate classes for metaschema: " + metaschemaUri, ex);
285 }
286 return context;
287 });
288 assert dynamicBindingContextFuture != null;
289
290 Future<IContentValidator> contentValidatorFuture = executor.submit(() -> {
291 Path schemaPath = generateSchemaFuture.get();
292 return getContentValidatorSupplier().apply(schemaPath);
293 });
294 assert contentValidatorFuture != null;
295
296
297 DynamicTest validateSchema = DynamicTest.dynamicTest(
298 "Validate Schema",
299 () -> {
300 Supplier<? extends IContentValidator> supplier = getSchemaValidatorSupplier();
301 if (supplier != null) {
302 Path schemaPath;
303 try {
304 schemaPath = ObjectUtils.requireNonNull(generateSchemaFuture.get());
305 } catch (ExecutionException ex) {
306 throw new JUnitException(
307 "failed to generate schema", ex.getCause());
308 }
309 validate(ObjectUtils.requireNonNull(supplier.get()), schemaPath);
310 }
311 });
312
313 Stream<? extends DynamicNode> contentTests;
314 {
315 contentTests = scenario.getValidationCaseList().stream()
316 .flatMap(contentCase -> {
317 assert contentCase != null;
318 DynamicTest test
319 = generateValidationCase(contentCase, dynamicBindingContextFuture, contentValidatorFuture,
320 collectionUri, ObjectUtils.notNull(scenarioGenerationPath));
321 return test == null ? Stream.empty() : Stream.of(test);
322 }).sequential();
323 }
324
325 return DynamicContainer.dynamicContainer(
326 scenario.getName(),
327 metaschemaUri,
328 Stream.concat(Stream.of(validateSchema), contentTests).sequential());
329 }
330
331 @SuppressWarnings("unchecked")
332 protected Path convertContent(URI contentUri, @NonNull Path generationPath, @NonNull IBindingContext context)
333 throws IOException {
334 Object object;
335 try {
336 object = context.newBoundLoader().load(ObjectUtils.notNull(contentUri.toURL()));
337 } catch (URISyntaxException ex) {
338 throw new IOException(ex);
339 }
340
341 if (!Files.exists(generationPath)) {
342 Files.createDirectories(generationPath);
343 }
344
345 Path convertedContetPath;
346 try {
347 convertedContetPath = ObjectUtils.notNull(Files.createTempFile(generationPath, "", "-content"));
348 } catch (IOException ex) {
349 throw new JUnitException("Unable to create schema temp file", ex);
350 }
351
352 @SuppressWarnings("rawtypes") ISerializer serializer
353 = context.newSerializer(getRequiredContentFormat(), object.getClass());
354 serializer.serialize(object, convertedContetPath, getWriteOpenOptions());
355
356 return convertedContetPath;
357 }
358
359 private DynamicTest generateValidationCase(
360 @NonNull ContentCaseType contentCase,
361 @NonNull Future<IBindingContext> contextFuture,
362 @NonNull Future<IContentValidator> contentValidatorFuture,
363 @NonNull URI collectionUri,
364 @NonNull Path generationPath) {
365
366 URI contentUri = collectionUri.resolve(contentCase.getLocation());
367
368 Format format = contentCase.getSourceFormat();
369 DynamicTest retval = null;
370 if (getRequiredContentFormat().equals(format)) {
371 retval = DynamicTest.dynamicTest(
372 String.format("Validate %s=%s: %s", format, contentCase.getValidationResult(),
373 contentCase.getLocation()),
374 contentUri,
375 () -> {
376 IContentValidator contentValidator;
377 try {
378 contentValidator = contentValidatorFuture.get();
379 } catch (ExecutionException ex) {
380 throw new JUnitException(
381 "failed to produce the content validator", ex.getCause());
382 }
383
384 assertEquals(
385 contentCase.getValidationResult(),
386 validate(
387 ObjectUtils.notNull(contentValidator), ObjectUtils.notNull(contentUri.toURL())),
388 "validation did not match expectation");
389 });
390 } else if (contentCase.getValidationResult()) {
391 retval = DynamicTest.dynamicTest(
392 String.format("Convert and Validate %s=%s: %s", format, contentCase.getValidationResult(),
393 contentCase.getLocation()),
394 contentUri,
395 () -> {
396 IBindingContext context;
397 try {
398 context = contextFuture.get();
399 } catch (ExecutionException ex) {
400 throw new JUnitException(
401 "failed to produce the content validator", ex.getCause());
402 }
403 assert context != null;
404
405 Path convertedContetPath;
406 try {
407 convertedContetPath = convertContent(contentUri, generationPath, context);
408 } catch (Exception ex) {
409 throw new JUnitException("failed to convert content: " + contentUri, ex);
410 }
411
412 IContentValidator contentValidator;
413 try {
414 contentValidator = contentValidatorFuture.get();
415 } catch (ExecutionException ex) {
416 throw new JUnitException(
417 "failed to produce the content validator",
418 ex.getCause());
419 }
420 assertEquals(contentCase.getValidationResult(),
421 validate(
422 ObjectUtils.notNull(contentValidator),
423 ObjectUtils.notNull(convertedContetPath.toUri().toURL())),
424 String.format("validation of '%s' did not match expectation", convertedContetPath));
425 });
426 }
427 return retval;
428 }
429
430 private static boolean validate(@NonNull IContentValidator validator, @NonNull URL target) throws IOException {
431 IValidationResult schemaValidationResult;
432 try {
433 schemaValidationResult = validator.validate(target);
434 } catch (URISyntaxException ex) {
435 throw new IOException(ex);
436 }
437 return processValidationResult(schemaValidationResult);
438 }
439
440 protected static boolean validate(@NonNull IContentValidator validator, @NonNull Path target) throws IOException {
441 IValidationResult schemaValidationResult = validator.validate(target);
442 if (!schemaValidationResult.isPassing()) {
443 LOGGER.atError().log("Schema validation failed for: {}", target);
444 }
445 return processValidationResult(schemaValidationResult);
446 }
447
448 private static boolean processValidationResult(IValidationResult schemaValidationResult) {
449 for (IValidationFinding finding : schemaValidationResult.getFindings()) {
450 logFinding(ObjectUtils.notNull(finding));
451 }
452 return schemaValidationResult.isPassing();
453 }
454
455 private static void logFinding(@NonNull IValidationFinding finding) {
456 LogBuilder logBuilder;
457 switch (finding.getSeverity()) {
458 case CRITICAL:
459 logBuilder = LOGGER.atFatal();
460 break;
461 case ERROR:
462 logBuilder = LOGGER.atError();
463 break;
464 case WARNING:
465 logBuilder = LOGGER.atWarn();
466 break;
467 case INFORMATIONAL:
468 logBuilder = LOGGER.atInfo();
469 break;
470 default:
471 throw new IllegalArgumentException("Unknown level: " + finding.getSeverity().name());
472 }
473
474
475
476
477
478 if (finding instanceof JsonValidationFinding) {
479 JsonValidationFinding jsonFinding = (JsonValidationFinding) finding;
480 logBuilder.log("[{}] {}", jsonFinding.getCause().getPointerToViolation(), finding.getMessage());
481 } else {
482 logBuilder.log("{}", finding.getMessage());
483 }
484 }
485 }