View Javadoc
1   /*
2    * Portions of this software was developed by employees of the National Institute
3    * of Standards and Technology (NIST), an agency of the Federal Government and is
4    * being made available as a public service. Pursuant to title 17 United States
5    * Code Section 105, works of NIST employees are not subject to copyright
6    * protection in the United States. This software may be subject to foreign
7    * copyright. Permission in the United States and in foreign countries, to the
8    * extent that NIST may hold copyright, to use, copy, modify, create derivative
9    * works, and distribute this software and its documentation without fee is hereby
10   * granted on a non-exclusive basis, provided that this notice and disclaimer
11   * of warranty appears in all copies.
12   *
13   * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
14   * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
15   * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
16   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
17   * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
18   * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
19   * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
20   * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
21   * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
22   * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
23   * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
24   * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
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( // NOPMD - this is not a webapp
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                     // directory iteration failed for some reason
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       // create the directories the schema will be stored in
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(); // NOPMD - intentional use of threads
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       // determine what file to use for the schema
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) { // NOPMD - intentional
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     // build a test container for the generate and validate steps
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( // NOPMD - cause is relevant, exception is not
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( // NOPMD - cause is relevant, exception is not
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( // NOPMD - cause is relevant, exception is not
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) { // NOPMD - intentional
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( // NOPMD - cause is relevant, exception is not
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     // if (finding.getCause() != null) {
475     // logBuilder.withThrowable(finding.getCause());
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 }