View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one   *
3    * or more contributor license agreements.  See the NOTICE file *
4    * distributed with this work for additional information        *
5    * regarding copyright ownership.  The ASF licenses this file   *
6    * to you under the Apache License, Version 2.0 (the            *
7    * "License"); you may not use this file except in compliance   *
8    * with the License.  You may obtain a copy of the License at   *
9    *                                                              *
10   *   https://www.apache.org/licenses/LICENSE-2.0                 *
11   *                                                              *
12   * Unless required by applicable law or agreed to in writing,   *
13   * software distributed under the License is distributed on an  *
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15   * KIND, either express or implied.  See the License for the    *
16   * specific language governing permissions and limitations      *
17   * under the License.                                           *
18   */
19  package org.apache.rat;
20  
21  import static org.assertj.core.api.Assertions.assertThat;
22  import static org.assertj.core.api.Assertions.assertThatThrownBy;
23  import static org.assertj.core.api.Fail.fail;
24  
25  import java.io.ByteArrayOutputStream;
26  import java.io.File;
27  import java.io.IOException;
28  import java.io.PrintStream;
29  import java.lang.reflect.Method;
30  import java.net.URISyntaxException;
31  import java.nio.charset.StandardCharsets;
32  
33  import java.nio.file.Files;
34  import java.nio.file.Path;
35  import java.util.Arrays;
36  import java.util.Collections;
37  import java.util.HashMap;
38  import java.util.List;
39  import java.util.Map;
40  import java.util.Optional;
41  import java.util.TreeMap;
42  import java.util.UUID;
43  import java.util.stream.Stream;
44  import java.util.regex.Pattern;
45  import javax.xml.XMLConstants;
46  import javax.xml.transform.Source;
47  import javax.xml.transform.TransformerException;
48  import javax.xml.transform.dom.DOMSource;
49  import javax.xml.transform.stream.StreamSource;
50  import javax.xml.validation.Schema;
51  import javax.xml.validation.SchemaFactory;
52  import javax.xml.validation.Validator;
53  import javax.xml.xpath.XPath;
54  import javax.xml.xpath.XPathConstants;
55  
56  import org.apache.commons.io.FileUtils;
57  import org.apache.commons.xml.secure.SecureSchemaFactory;
58  import org.apache.commons.xml.secure.SecureXPathFactory;
59  import org.apache.rat.api.Document.Type;
60  import org.apache.rat.api.RatException;
61  import org.apache.rat.commandline.Arg;
62  import org.apache.rat.commandline.ArgumentContext;
63  import org.apache.rat.document.FileDocument;
64  import org.apache.rat.document.DocumentName;
65  import org.apache.rat.license.ILicenseFamily;
66  import org.apache.rat.report.claim.ClaimStatistic;
67  import org.apache.rat.report.claim.ClaimStatisticTest;
68  import org.apache.rat.test.utils.Resources;
69  import org.apache.rat.testhelpers.BaseOption;
70  import org.apache.rat.testhelpers.BaseOptionCollection;
71  import org.apache.rat.testhelpers.XmlUtils;
72  import org.apache.rat.testhelpers.data.ReportTestDataProvider;
73  import org.apache.rat.testhelpers.data.TestData;
74  import org.apache.rat.testhelpers.data.ValidatorData;
75  import org.apache.rat.utils.StandardXmlFactory;
76  import org.apache.rat.walker.DirectoryWalker;
77  import org.junit.jupiter.api.AfterAll;
78  import org.junit.jupiter.api.BeforeAll;
79  import org.junit.jupiter.api.BeforeEach;
80  import org.junit.jupiter.api.Test;
81  import org.junit.jupiter.api.TestInfo;
82  import org.junit.jupiter.params.ParameterizedTest;
83  import org.junit.jupiter.params.provider.Arguments;
84  import org.junit.jupiter.params.provider.MethodSource;
85  import org.w3c.dom.Document;
86  import org.w3c.dom.NodeList;
87  import org.xml.sax.SAXException;
88  
89  /**
90   * Tests the output of the Reporter.
91   */
92  public class ReporterTest {
93      /**
94       * Temporary file. Not using tempDir because it does not always work properly.
95       */
96      private static Path tempPath;
97  
98      /**
99       * The directory for the test.
100      */
101     private Path testPath;
102 
103     /**
104      * Directory for the test data.
105      */
106     final String basedir;
107 
108     private final OptionCollectionParser<BaseOption> collectionParser;
109 
110     ReporterTest() throws URISyntaxException {
111         basedir = Resources.getExampleResource("exampleData").getPath();
112         collectionParser = new OptionCollectionParser<>(new BaseOptionCollection());
113     }
114 
115     @BeforeAll
116     static void setUp() throws IOException {
117         tempPath = Files.createTempDirectory("ReporterTest");
118         Arg.reset();
119     }
120 
121     @AfterAll
122     static void cleanup() throws IOException {
123         FileUtils.deleteDirectory(tempPath.toFile());
124     }
125 
126     @BeforeEach
127     void setUpTest(TestInfo testInfo) {
128         Optional<Method> testMethod = testInfo.getTestMethod();
129         this.testPath = tempPath.resolve(testMethod.map(Method::getName).orElseGet(() -> UUID.randomUUID().toString()));
130         File file = testPath.toFile();
131         if (!file.exists()) {
132             if (!file.mkdirs()) {
133                 throw new RuntimeException("Unable to create temp directory: " + file.getName());
134             }
135         } else {
136             if (!file.isDirectory()) {
137                 throw new RuntimeException(file.getName() + " is NOT a directory.");
138             }
139         }
140     }
141 
142     @Test
143     void testExecute() throws RatException {
144         File output = testPath.resolve("output.xml").toFile();
145         ArgumentContext ctxt = collectionParser.parseCommands(new File("."), new String[]{"--output-style", "xml", "--output-file", output.getPath(), basedir});
146         ClaimStatistic statistic = new Reporter(ctxt.getConfiguration()).execute().getStatistic();
147 
148         assertThat(statistic.getCounter(Type.ARCHIVE)).isEqualTo(1);
149         assertThat(statistic.getCounter(Type.BINARY)).isEqualTo(2);
150         assertThat(statistic.getCounter(Type.IGNORED)).isEqualTo(2);
151         assertThat(statistic.getCounter(Type.NOTICE)).isEqualTo(2);
152         assertThat(statistic.getCounter(Type.STANDARD)).isEqualTo(8);
153         assertThat(statistic.getCounter(Type.UNKNOWN)).isEqualTo(0);
154         assertThat(statistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(8);
155         assertThat(statistic.getCounter(ClaimStatistic.Counter.ARCHIVES)).isEqualTo(1);
156         assertThat(statistic.getCounter(ClaimStatistic.Counter.BINARIES)).isEqualTo(2);
157         assertThat(statistic.getCounter(ClaimStatistic.Counter.DOCUMENT_TYPES)).isEqualTo(5);
158         assertThat(statistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(2);
159         assertThat(statistic.getCounter(ClaimStatistic.Counter.LICENSE_CATEGORIES)).isEqualTo(4);
160         assertThat(statistic.getCounter(ClaimStatistic.Counter.LICENSE_NAMES)).isEqualTo(5);
161         assertThat(statistic.getCounter(ClaimStatistic.Counter.NOTICES)).isEqualTo(2);
162         assertThat(statistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(8);
163         assertThat(statistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(2);
164         assertThat(statistic.getCounter(ClaimStatistic.Counter.UNKNOWN)).isEqualTo(2);
165 
166         List<Type> typeList = statistic.getDocumentTypes();
167         assertThat(typeList).isEqualTo(Arrays.asList(Type.ARCHIVE, Type.BINARY, Type.IGNORED, Type.NOTICE, Type.STANDARD));
168 
169         TreeMap<String, Integer> expected = new TreeMap<>();
170         expected.put("Unknown license", 2);
171         expected.put("Apache License 2.0", 5);
172         expected.put("The MIT License", 1);
173         expected.put("BSD 3 clause", 1);
174         expected.put("The Telemanagement Forum License", 1);
175         TreeMap<String, Integer> actual = new TreeMap<>();
176 
177         for (String licenseName : statistic.getLicenseNames()) {
178             actual.put(licenseName, statistic.getLicenseNameCount(licenseName));
179         }
180         assertThat(actual).isEqualTo(expected);
181 
182         expected.clear();
183         expected.put("?????", 2);
184         expected.put("AL   ", 5);
185         expected.put("BSD-3", 2);
186         expected.put("MIT  ", 1);
187         actual.clear();
188         for (String licenseCategory : statistic.getLicenseFamilyCategories()) {
189             actual.put(licenseCategory, statistic.getLicenseCategoryCount(licenseCategory));
190         }
191         assertThat(actual).isEqualTo(expected);
192     }
193 
194     @Test
195     void testExecuteNoSource() throws RatException, TransformerException {
196         File output = testPath.resolve("testExecuteNoSource").toFile();
197         ArgumentContext context = collectionParser.parseCommands(new File("."), new String[]{"--output-style", "xml", "--output-file", output.getPath()});
198         ReportConfiguration config = OptionCollection.createConfiguration(context);
199         Reporter.Output result = new Reporter(config).execute();
200         assertThat(StandardXmlFactory.serializeDocument(result.getDocument())).isEmpty();
201         ClaimStatisticTest.assertSame(result.getStatistic(), new ClaimStatistic());
202     }
203 
204     @Test
205     void testOutputOption() throws Exception {
206         File output = testPath.resolve("testOutputOption.txt").toFile();
207         ArgumentContext ctxt = collectionParser.parseCommands(new File("."), new String[]{"--output-file", output.getCanonicalPath(), basedir});
208         new Reporter(ctxt.getConfiguration()).execute().format(ctxt.getConfiguration());
209         assertThat(output.exists()).isTrue();
210         String content = FileUtils.readFileToString(output, StandardCharsets.UTF_8);
211         assertThat(content).containsPattern(Pattern.compile("^! Unapproved:\\s*2 ", Pattern.MULTILINE))
212                 .contains("/Source.java")
213                 .contains("/sub/Empty.txt");
214     }
215 
216     @Test
217     void testGetOutputMethod() throws Exception {
218         File output = testPath.resolve("testGetOutputMethod.txt").toFile();
219         ArgumentContext context = collectionParser.parseCommands(new File("."), new String[]{"-o", output.getCanonicalPath(), basedir});
220         Reporter reporter = new Reporter(context.getConfiguration());
221         Reporter.Output expected = reporter.execute();
222         assertThat(reporter.getOutput()).isEqualTo(expected);
223     }
224 
225     @Test
226     void testDefaultOutput() throws Exception {
227         File output = testPath.resolve("captured.txt").toFile();
228 
229         PrintStream origin = System.out;
230         try (PrintStream out = new PrintStream(output)) {
231             System.setOut(out);
232             ArgumentContext ctxt = collectionParser.parseCommands(new File("."), new String[]{basedir});
233             new Reporter(ctxt.getConfiguration()).execute().format(ctxt.getConfiguration());
234         } finally {
235             System.setOut(origin);
236         }
237         assertThat(output).exists();
238         String content = FileUtils.readFileToString(output, StandardCharsets.UTF_8);
239         verifyStandardContent(content);
240     }
241     
242     private static Map<String, String> mapOf(String... parts) {
243         Map<String, String> map = new HashMap<>();
244         for (int i = 0; i < parts.length; i+=2) {
245             map.put(parts[i], parts[i+1]);
246         }
247         return map;
248     }
249 
250     @Test
251     void testXMLOutput() throws Exception {
252         Map<String, Map<String, String>> expected = new HashMap<>();
253         expected.put("/.hiddenDirectory", mapOf("isDirectory", "true", "mediaType", "application/octet-stream",
254                 "type", "IGNORED"));
255         expected.put("/ILoggerFactory.java", mapOf("encoding", "windows-1252", "mediaType", "text/x-java-source",
256                 "type", "STANDARD"));
257         expected.put("/Image.png", mapOf("mediaType", "image/png", "type", "BINARY"));
258         expected.put("/LICENSE", mapOf("encoding", "windows-1252", "mediaType", "text/plain", "type", "NOTICE"));
259         expected.put("/NOTICE", mapOf("encoding", "windows-1252", "mediaType", "text/plain", "type", "NOTICE"));
260         expected.put("/Source.java", mapOf("encoding", "windows-1252", "mediaType", "text/x-java-source",
261                 "type", "STANDARD"));
262         expected.put("/Text.txt", mapOf("encoding", "windows-1252", "mediaType", "text/plain",
263                 "type", "STANDARD"));
264         expected.put("/TextHttps.txt", mapOf("encoding", "windows-1252", "mediaType", "text/plain",
265                 "type", "STANDARD"));
266         expected.put("/Xml.xml", mapOf("encoding", "windows-1252", "mediaType", "application/xml",
267                 "type", "STANDARD"));
268         expected.put("/buildr.rb", mapOf("encoding", "windows-1252", "mediaType", "text/x-ruby",
269                 "type", "STANDARD"));
270         expected.put("/dummy.jar", mapOf("mediaType", "application/java-archive",
271                 "type", "ARCHIVE"));
272         expected.put("/generated.txt", mapOf("encoding", "windows-1252", "mediaType", "text/plain",
273                 "type", "IGNORED"));
274         expected.put("/plain.json", mapOf("mediaType", "application/json",
275                 "type", "BINARY"));
276         expected.put("/sub/Empty.txt", mapOf("encoding", "windows-1252", "mediaType", "text/plain",
277                 "type", "STANDARD"));
278         expected.put("/tri.txt", mapOf("encoding", "windows-1252", "mediaType", "text/plain",
279                 "type", "STANDARD"));
280 
281         File output = testPath.resolve(".rat/testXMLOutput").toFile();
282         org.apache.rat.utils.FileUtils.mkDir(output.getParentFile());
283         ArgumentContext ctxt = collectionParser.parseCommands(testPath.toFile(), new String[]{"--output-style", "xml", "--output-file", output.getPath(), basedir});
284         new Reporter(ctxt.getConfiguration()).execute().format(ctxt.getConfiguration());
285 
286         assertThat(output).exists();
287         Document doc = XmlUtils.toDom(java.nio.file.Files.newInputStream(output.toPath()));
288         XPath xPath = SecureXPathFactory.newInstance().newXPath();
289 
290         for (Map.Entry<String, Map<String, String>> entry : expected.entrySet()) {
291             XmlUtils.assertAttributes(doc, xPath, String.format("/rat-report/resource[@name='%s']", entry.getKey()), entry.getValue());
292         }
293 
294         NodeList nodeList = XmlUtils.getNodeList(doc, xPath, "/rat-report/resource/license[@approval='false']");
295         assertThat(nodeList.getLength()).isEqualTo(2);
296 
297         nodeList = XmlUtils.getNodeList(doc, xPath, "/rat-report/resource/license[@id='AL2.0']");
298         assertThat(nodeList.getLength()).isEqualTo(5);
299 
300         nodeList = XmlUtils.getNodeList(doc, xPath, "/rat-report/resource/license[@id='MIT']");
301         assertThat(nodeList.getLength()).isEqualTo(1);
302 
303         nodeList = XmlUtils.getNodeList(doc, xPath, "/rat-report/resource/license[@id='BSD-3']");
304         assertThat(nodeList.getLength()).isEqualTo(1);
305 
306         nodeList = XmlUtils.getNodeList(doc, xPath, "/rat-report/resource/license[@id='TMF']");
307         assertThat(nodeList.getLength()).isEqualTo(1);
308 
309         nodeList = XmlUtils.getNodeList(doc, xPath, "/rat-report/resource/license[@id='?????']");
310         assertThat(nodeList.getLength()).isEqualTo(2);
311 
312         nodeList = XmlUtils.getNodeList(doc, xPath, "/rat-report/resource[@type='STANDARD']");
313         assertThat(nodeList.getLength()).isEqualTo(8);
314 
315         nodeList = XmlUtils.getNodeList(doc, xPath, "/rat-report/resource[@type='ARCHIVE']");
316         assertThat(nodeList.getLength()).isEqualTo(1);
317 
318         nodeList = XmlUtils.getNodeList(doc, xPath, "/rat-report/resource[@type='BINARY']");
319         assertThat(nodeList.getLength()).isEqualTo(2);
320 
321         nodeList = XmlUtils.getNodeList(doc, xPath, "/rat-report/resource[@type='IGNORED']");
322         assertThat(nodeList.getLength()).isEqualTo(2);
323 
324         nodeList = XmlUtils.getNodeList(doc, xPath, "/rat-report/resource[@type='UNKNOWN']");
325         assertThat(nodeList.getLength()).isEqualTo(0);
326 
327         nodeList = XmlUtils.getNodeList(doc, xPath, "/rat-report/resource[@type='NOTICE']");
328         assertThat(nodeList.getLength()).isEqualTo(2);
329 
330         nodeList = XmlUtils.getNodeList(doc, xPath, "/rat-report/resource[@type='IGNORED']/license");
331         assertThat(nodeList.getLength()).isEqualTo(0);
332 
333         nodeList = XmlUtils.getNodeList(doc, xPath, "/rat-report/resource[@isDirectory='true']");
334         assertThat(nodeList.getLength()).isEqualTo(1);
335 
336         // check licenses
337         Map<String, String> apacheLicense = mapOf("approval", "true", "family", "AL   ", "id", "AL2.0", "name", "Apache License 2.0");
338         Map<String, String> unknownLicense = mapOf("approval", "false", "family", "?????",
339                 "id", "?????", "name", "Unknown license");
340         expected = new HashMap<>();
341         expected.put("/.hiddenDirectory", Collections.emptyMap());
342         expected.put("/ILoggerFactory.java", mapOf("approval", "true", "family", "MIT  ",
343                 "id", "MIT", "name", "The MIT License"));
344         expected.put("/Image/png", Collections.emptyMap());
345         expected.put("/LICENSE", Collections.emptyMap());
346         expected.put("/NOTICE", Collections.emptyMap());
347         expected.put("/Source.java", unknownLicense);
348         expected.put("/Text.txt", apacheLicense);
349         expected.put("/TextHttps.txt", apacheLicense);
350         expected.put("/Xml.xml", apacheLicense);
351         expected.put("/buildr.rb", apacheLicense);
352         expected.put("/dummy.jar", Collections.emptyMap());
353         expected.put("/generated.txt", Collections.emptyMap());
354         expected.put("/plain.json", Collections.emptyMap());
355         expected.put("/sub/Empty.txt", unknownLicense);
356         expected.put("/tri.txt", mapOf("approval", "true", "family", "BSD-3", "id", "TMF",
357                 "name", "The Telemanagement Forum License"));
358 
359         for (Map.Entry<String, Map<String, String>> entry : expected.entrySet()) {
360             Map<String, String> attrs = entry.getValue();
361             if (attrs.isEmpty()) {
362                 String xpath = String.format("/rat-report/resource[@name='%s']/license",entry.getKey());
363                 nodeList = XmlUtils.getNodeList(doc, xPath, xpath);
364                 assertThat(nodeList.getLength()).as(xpath).isEqualTo(0);
365             } else {
366                 XmlUtils.assertAttributes(doc, xPath, String.format("/rat-report/resource[@name='%s']/license[@id='%s']",
367                         entry.getKey(), attrs.get("id")), attrs);
368             }
369         }
370     }
371 
372     /**
373      * Finds a node via xpath on the document. And then checks family, approval and
374      * type of elements of the node.
375      *
376      * @param doc the document to check
377      * @param xpath the XPath instance to use.
378      * @param resource the XPath statement to locate the node.
379      * @param licenseInfo the license info for the node (can be {@code null}).
380      * @param type the type of resource located.
381      * @throws Exception on XPath error.
382      */
383     private static void checkNode(final Document doc, final XPath xpath, final String resource, final LicenseInfo licenseInfo,
384                                   final String type) throws Exception {
385         XmlUtils.getNode(doc, xpath, String.format("/rat-report/resource[@name='%s'][@type='%s']", resource, type));
386         if (licenseInfo != null) {
387             XmlUtils.getNode(doc, xpath,
388                     String.format("/rat-report/resource[@name='%s'][@type='%s']/license[@id='%s'][@family='%s']",
389                             resource, type, licenseInfo.id, licenseInfo.family));
390             XmlUtils.getNode(doc, xpath,
391                     String.format("/rat-report/resource[@name='%s'][@type='%s']/license[@id='%s'][@approval='%s']",
392                             resource, type, licenseInfo.id, licenseInfo.approval));
393             if (licenseInfo.hasNotes) {
394                 XmlUtils.getNode(doc, xpath,
395                         String.format("/rat-report/resource[@name='%s'][@type='%s']/license[@id='%s']/notes", resource,
396                                 type, licenseInfo.id));
397             }
398         }
399     }
400 
401     private ReportConfiguration initializeConfiguration() throws URISyntaxException {
402         Defaults defaults = Defaults.builder().build();
403         final File elementsFile = Resources.getExampleResource("exampleData");
404         final ReportConfiguration configuration = new ReportConfiguration();
405         configuration.setFrom(defaults);
406         DocumentName documentName = DocumentName.builder(elementsFile).build();
407         configuration.addSource(new DirectoryWalker(new FileDocument(documentName, elementsFile,
408                 configuration.getDocumentExcluder(documentName))));
409         return configuration;
410     }
411 
412     private void verifyStandardContent(final String document) {
413         assertThat(document)
414                 .containsPattern(Pattern.compile("^ {2}Notices:\\s*2 ", Pattern.MULTILINE))
415                 .containsPattern(Pattern.compile("^ {2}Binaries:\\s*2 ", Pattern.MULTILINE))
416                 .containsPattern(Pattern.compile("^ {2}Archives:\\s*1 ", Pattern.MULTILINE))
417                 .containsPattern(Pattern.compile("^ {2}Standards:\\s*8 ", Pattern.MULTILINE))
418                 .containsPattern(Pattern.compile("^ {2}Ignored:\\s*2 ", Pattern.MULTILINE))
419                 .containsPattern(Pattern.compile("^! Unapproved:\\s*2 ", Pattern.MULTILINE))
420                 .containsPattern(Pattern.compile("^ {2}Unknown:\\s*2 ", Pattern.MULTILINE))
421                 .containsPattern(Pattern.compile("^Apache License 2.0: 5 ", Pattern.MULTILINE))
422                 .containsPattern(Pattern.compile("^BSD 3 clause: 1 ", Pattern.MULTILINE))
423                 .containsPattern(Pattern.compile("^The MIT License: 1 ", Pattern.MULTILINE))
424                 .containsPattern(Pattern.compile("^The Telemanagement Forum License: 1 ", Pattern.MULTILINE))
425                 .containsPattern(Pattern.compile("^Unknown license: 2 ", Pattern.MULTILINE))
426                 .containsPattern(Pattern.compile("^\\Q?????\\E: 2 ", Pattern.MULTILINE))
427                 .containsPattern(Pattern.compile("^AL {3}: 5 ", Pattern.MULTILINE))
428                 .containsPattern(Pattern.compile("^BSD-3: 2 ", Pattern.MULTILINE))
429                 .containsPattern(Pattern.compile("^MIT {2}: 1 ", Pattern.MULTILINE))
430                 .containsPattern(
431                         Pattern.compile("^Files with unapproved licenses\\s+\\*+\\s+" //
432                         + "\\Q/Source.java\\E\\s+" //
433                                 + "\\Q/sub/Empty.txt\\E\\s", Pattern.MULTILINE))
434                 .containsPattern(Pattern.compile(ReporterTestUtils.documentOut(true, Type.ARCHIVE, "/dummy.jar"), Pattern.MULTILINE))
435                 .containsPattern(
436                         Pattern.compile(ReporterTestUtils.documentOut(true, Type.STANDARD, "/ILoggerFactory.java")
437                                 + ReporterTestUtils.licenseOut("MIT", "The MIT License"), Pattern.MULTILINE))
438                 .containsPattern(Pattern.compile(ReporterTestUtils.documentOut(true, Type.BINARY, "/Image.png"), Pattern.MULTILINE))
439                 .containsPattern(Pattern.compile(ReporterTestUtils.documentOut(true, Type.NOTICE, "/LICENSE"), Pattern.MULTILINE))
440                 .containsPattern(Pattern.compile(ReporterTestUtils.documentOut(true, Type.NOTICE, "/NOTICE"), Pattern.MULTILINE))
441                 .containsPattern(Pattern.compile(ReporterTestUtils.documentOut(false, Type.STANDARD, "/Source.java")
442                         + ReporterTestUtils.UNKNOWN_LICENSE, Pattern.MULTILINE))
443                 .containsPattern(Pattern.compile(ReporterTestUtils.documentOut(true, Type.STANDARD, "/Text.txt")
444                         + ReporterTestUtils.APACHE_LICENSE, Pattern.MULTILINE))
445                 .containsPattern(Pattern.compile(ReporterTestUtils.documentOut(true, Type.STANDARD, "/Xml.xml")
446                         + ReporterTestUtils.APACHE_LICENSE, Pattern.MULTILINE))
447                 .containsPattern(Pattern.compile(ReporterTestUtils.documentOut(true, Type.STANDARD, "/buildr.rb")
448                         + ReporterTestUtils.APACHE_LICENSE, Pattern.MULTILINE))
449                 .containsPattern(Pattern.compile(ReporterTestUtils.documentOut(true, Type.STANDARD, "/TextHttps.txt")
450                         + ReporterTestUtils.APACHE_LICENSE, Pattern.MULTILINE))
451                 .containsPattern(Pattern.compile(ReporterTestUtils.documentOut(true, Type.STANDARD, "/tri.txt")
452                 + ReporterTestUtils.APACHE_LICENSE + ReporterTestUtils.licenseOut("BSD-3", "BSD 3 clause")
453                         + ReporterTestUtils.licenseOut("BSD-3", "TMF", "The Telemanagement Forum License"), Pattern.MULTILINE))
454                 .containsPattern(Pattern.compile(ReporterTestUtils.documentOut(false, Type.STANDARD, "/sub/Empty.txt")
455                         + ReporterTestUtils.UNKNOWN_LICENSE, Pattern.MULTILINE));
456     }
457 
458     private Validator initValidator() throws SAXException {
459         SchemaFactory factory = SecureSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
460         Source schemaFile = new StreamSource(Reporter.class.getResourceAsStream("/org/apache/rat/rat-report.xsd"));
461         Schema schema = factory.newSchema(schemaFile);
462         return schema.newValidator();
463     }
464 
465     @Test
466     void xmlReportTest() throws Exception {
467         ReportConfiguration configuration = initializeConfiguration();
468         Document doc = new Reporter(configuration).execute().getDocument();
469 
470         XPath xPath = SecureXPathFactory.newInstance().newXPath();
471 
472         XmlUtils.getNode(doc, xPath, "/rat-report[@timestamp]");
473 
474         LicenseInfo apacheLic = new LicenseInfo("AL2.0", "AL", true, false);
475         checkNode(doc, xPath, "/ILoggerFactory.java", new LicenseInfo("MIT", true, false),
476                 "STANDARD");
477         checkNode(doc, xPath, "/Image.png", null, "BINARY");
478         checkNode(doc, xPath, "/LICENSE", null, "NOTICE");
479         checkNode(doc, xPath, "/NOTICE", null, "NOTICE");
480         checkNode(doc, xPath, "/Source.java", new LicenseInfo("?????", false, false),
481                 "STANDARD");
482         checkNode(doc, xPath, "/Text.txt", apacheLic, "STANDARD");
483         checkNode(doc, xPath, "/TextHttps.txt", apacheLic, "STANDARD");
484         checkNode(doc, xPath, "/Xml.xml", apacheLic, "STANDARD");
485         checkNode(doc, xPath, "/buildr.rb", apacheLic, "STANDARD");
486         checkNode(doc, xPath, "/dummy.jar", null, "ARCHIVE");
487         checkNode(doc, xPath, "/sub/Empty.txt", new LicenseInfo("?????", false, false),
488                 "STANDARD");
489         checkNode(doc, xPath, "/tri.txt", apacheLic, "STANDARD");
490         checkNode(doc, xPath, "/tri.txt", new LicenseInfo("BSD-3", true, false), "STANDARD");
491         checkNode(doc, xPath, "/tri.txt", new LicenseInfo("TMF", "BSD-3", true, false),
492                 "STANDARD");
493         checkNode(doc, xPath, "/generated.txt", null, "IGNORED");
494         NodeList nodeList = (NodeList) xPath.compile("/rat-report/resource").evaluate(doc, XPathConstants.NODESET);
495         assertThat(nodeList.getLength()).isEqualTo(15);
496         Validator validator = initValidator();
497         try {
498             validator.validate(new DOMSource(doc));
499         } catch (SAXException e) {
500             fail("Missing properties?", e);
501         }
502     }
503 
504     @Test
505     void plainReportTest() throws Exception {
506         final String NL = System.lineSeparator();
507         final String SEPARATOR = "*****************************************************";
508         final String HEADER = SEPARATOR + NL + //
509                 "Summary" + NL + //
510                 SEPARATOR + NL + //
511                 "Generated at: ";
512         ByteArrayOutputStream out = new ByteArrayOutputStream();
513         ReportConfiguration configuration = initializeConfiguration();
514         configuration.setOutput(new ReportConfiguration.IODescriptor<>("plainReportTest", () -> out));
515         new Reporter(configuration).execute().format(configuration);
516 
517         String document = out.toString();
518 
519         assertThat(document).as(() -> "'Generated at' is not present in \n" + document)
520                 .doesNotContainPattern("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
521                 .startsWith(HEADER);
522 
523         verifyStandardContent(document);
524     }
525 
526     @Test
527     void unapprovedLicensesReportTest() throws Exception {
528         ByteArrayOutputStream out = new ByteArrayOutputStream();
529         ReportConfiguration configuration = initializeConfiguration();
530         configuration.setOutput(new ReportConfiguration.IODescriptor<>("unapprovedLicensesReportTest", () -> out));
531         configuration.setStyleSheet(this.getClass().getResource("/org/apache/rat/unapproved-licenses.xsl"));
532         new Reporter(configuration).execute().format(configuration);
533 
534         String document = out.toString();
535 
536         assertThat(document).containsOnlyOnce("Generated at: ")
537                 .containsPattern(Pattern.compile("\\Q/Source.java\\E$", Pattern.MULTILINE))
538                 .containsPattern(Pattern.compile("\\Q/sub/Empty.txt\\E", Pattern.MULTILINE));
539     }
540 
541     @Test
542     void counterMaxTest() throws Exception {
543         ReportConfiguration config = initializeConfiguration();
544         Reporter.Output output = new Reporter(config).execute();
545         assertThat(config.getClaimValidator().hasErrors()).isTrue();
546         assertThat(config.getClaimValidator().isValid(ClaimStatistic.Counter.UNAPPROVED, output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)))
547                 .isFalse();
548 
549         config = initializeConfiguration();
550         config.getClaimValidator().setMax(ClaimStatistic.Counter.UNAPPROVED, 2);
551         output = new Reporter(config).execute();
552         assertThat(config.getClaimValidator().hasErrors()).isFalse();
553         assertThat(config.getClaimValidator().isValid(ClaimStatistic.Counter.UNAPPROVED, output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)))
554                 .isTrue();
555     }
556 
557     static Stream<Arguments> getTestData() {
558         BaseOptionCollection.Builder builder = BaseOptionCollection.builder()
559                         .unsupported(Arg.OUTPUT_FILE);
560         return new ReportTestDataProvider().getOptionTests(builder.build()).stream().map(testData ->
561                 Arguments.of(testData.getTestName(), testData));
562     }
563 
564     @ParameterizedTest( name = "{index} {0}")
565     @MethodSource("getTestData")
566     void testReportData(String name, TestData test) throws Exception {
567         Path invokePath = testPath.resolve(test.getTestName());
568         org.apache.rat.utils.FileUtils.mkDir(invokePath.toFile());
569 
570         test.setupFiles(invokePath);
571         ArgumentContext ctxt = collectionParser.parseCommands(invokePath.toFile(),
572                 test.getCommandLine(invokePath.toString()));
573         if (test.expectingException()) {
574             assertThatThrownBy(() -> new Reporter(ctxt.getConfiguration()).execute()).as("Expected throws from " + name)
575                     .hasMessageContaining(test.getExpectedException().getMessage());
576             ValidatorData data = new ValidatorData(Reporter.Output.builder().configuration(ctxt.getConfiguration()).build(),
577                     invokePath.toString());
578             test.getValidator().accept(data);
579         } else {
580             Reporter.Output output = ctxt.getConfiguration() != null ? new Reporter(ctxt.getConfiguration()).execute() :
581                     Reporter.Output.builder().build();
582             ValidatorData data = new ValidatorData(output, invokePath.toString());
583             data.getOutput().format(data.getConfiguration());
584             test.getValidator().accept(data);
585         }
586     }
587 
588     private record LicenseInfo(String id, String family, boolean approval, boolean hasNotes) {
589         LicenseInfo(String id, boolean approval, boolean hasNotes) {
590             this(id, id, approval, hasNotes);
591         }
592 
593         private LicenseInfo(String id, String family, boolean approval, boolean hasNotes) {
594             this.id = id;
595             this.family = ILicenseFamily.makeCategory(family);
596             this.approval = approval;
597             this.hasNotes = hasNotes;
598         }
599     }
600 }