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