diff --git a/apache-rat-core/pom.xml b/apache-rat-core/pom.xml index 21f0c583c..061670044 100644 --- a/apache-rat-core/pom.xml +++ b/apache-rat-core/pom.xml @@ -263,5 +263,10 @@ jimfs test + + org.xmlunit + xmlunit-assertj3 + test + diff --git a/apache-rat-core/src/it/java/org/apache/rat/ReportTest.java b/apache-rat-core/src/it/java/org/apache/rat/ReportTest.java index b695134a6..34cf3909d 100644 --- a/apache-rat-core/src/it/java/org/apache/rat/ReportTest.java +++ b/apache-rat-core/src/it/java/org/apache/rat/ReportTest.java @@ -80,7 +80,7 @@ * associated with the exception. * */ -public class ReportTest { +class ReportTest { private String[] asArgs(final List argsList) { return argsList.toArray(new String[0]); @@ -88,7 +88,7 @@ private String[] asArgs(final List argsList) { @ParameterizedTest(name = "{index} {0}") @MethodSource("args") - public void integrationTest(String testName, Document commandLineDoc) throws Exception { + void integrationTest(String testName, Document commandLineDoc) throws Exception { DefaultLog.getInstance().log(Log.Level.INFO, "Running test for " + testName); File baseDir = new File(commandLineDoc.getName().getName()).getParentFile(); @@ -119,9 +119,11 @@ public void integrationTest(String testName, Document commandLineDoc) throws Exc File expectedMsg = new File(baseDir, "expected-message.txt"); if (expectedMsg.exists()) { - String msg = IOUtils.readLines(new FileReader(expectedMsg)).get(0).trim(); - assertThrows(RatDocumentAnalysisException.class, () -> Report.main(asArgs(argsList)), - msg); + try (FileReader fr = new FileReader(expectedMsg)) { + String msg = IOUtils.readLines(fr).get(0).trim(); + assertThrows(RatDocumentAnalysisException.class, () -> Report.main(asArgs(argsList)), + msg); + } } else { Report.main(asArgs(argsList)); } @@ -142,7 +144,7 @@ public void integrationTest(String testName, Document commandLineDoc) throws Exc try { Object value = shell.run(groovyScript, new String[]{outputFile.getAbsolutePath(), logFile.getAbsolutePath()}); if (value != null) { - fail(String.format("%s", value)); + fail(String.format("%s: %s", testName, value)); } } catch (AssertionError e) { throw new AssertionError(String.format("%s: %s", testName, e.getMessage()), e); @@ -204,6 +206,7 @@ public static class FileLog implements Log { * * @param level the level to use when writing messages. */ + @Override public void setLevel(final Level level) { this.level = level; } diff --git a/apache-rat-core/src/it/resources/ReportTest/RAT_14/verify.groovy b/apache-rat-core/src/it/resources/ReportTest/RAT_14/verify.groovy index 226394df0..c0027ba13 100644 --- a/apache-rat-core/src/it/resources/ReportTest/RAT_14/verify.groovy +++ b/apache-rat-core/src/it/resources/ReportTest/RAT_14/verify.groovy @@ -66,9 +66,9 @@ myArgs[3] = src.getAbsolutePath() ReportConfiguration configuration = OptionCollection.parseCommands(src, myArgs, { opts -> }) assertNotNull(configuration) -configuration.validate(DefaultLog.getInstance().&error) +configuration.validate() Reporter reporter = new Reporter(configuration) -ClaimStatistic statistic = reporter.execute() +ClaimStatistic statistic = reporter.execute().getStatistic() assertEquals(3, statistic.getCounter(ClaimStatistic.Counter.APPROVED)) assertEquals(2, statistic.getCounter(ClaimStatistic.Counter.ARCHIVES)) diff --git a/apache-rat-core/src/it/resources/ReportTest/RAT_362/expected-message.txt b/apache-rat-core/src/it/resources/ReportTest/RAT_362/expected-message.txt index bea4ba545..7e187ea8d 100644 --- a/apache-rat-core/src/it/resources/ReportTest/RAT_362/expected-message.txt +++ b/apache-rat-core/src/it/resources/ReportTest/RAT_362/expected-message.txt @@ -1 +1 @@ -Issues with UNAPPROVED +Issues with LICENSE_CATEGORIES, LICENSE_NAMES, STANDARDS diff --git a/apache-rat-core/src/it/resources/ReportTest/RAT_406/commandLine.txt b/apache-rat-core/src/it/resources/ReportTest/RAT_406/commandLine.txt index 0d6433f57..cec3b5e30 100644 --- a/apache-rat-core/src/it/resources/ReportTest/RAT_406/commandLine.txt +++ b/apache-rat-core/src/it/resources/ReportTest/RAT_406/commandLine.txt @@ -1,2 +1,3 @@ --licenses-denied DOJO +-- diff --git a/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java b/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java index 2d4bdd636..9a6066b6f 100644 --- a/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java +++ b/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java @@ -35,7 +35,6 @@ import java.util.stream.Collectors; import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; @@ -132,27 +131,22 @@ public static ReportConfiguration parseCommands(final File workingDirectory, fin final Consumer helpCmd, final boolean noArgs) throws IOException { Options opts = buildOptions(); - CommandLine commandLine; + ArgumentContext argumentContext; try { - commandLine = DefaultParser.builder().setDeprecatedHandler(DeprecationReporter.getLogReporter()) - .setAllowPartialMatching(true).build().parse(opts, args); + argumentContext = new ArgumentContext(workingDirectory, opts, args); } catch (ParseException e) { - DefaultLog.getInstance().error(e.getMessage()); - DefaultLog.getInstance().error("Please use the \"--help\" option to see a list of valid commands and options.", e); System.exit(1); return null; // dummy return (won't be reached) to avoid Eclipse complaint about possible NPE // for "commandLine" } - - ArgumentContext argumentContext = new ArgumentContext(workingDirectory, commandLine); Arg.processLogLevel(argumentContext, CLIOptionCollection.INSTANCE); - if (commandLine.hasOption(HELP)) { + if (argumentContext.getCommandLine().hasOption(HELP)) { helpCmd.accept(opts); return null; } - if (commandLine.hasOption(Arg.HELP_LICENSES.option())) { + if (argumentContext.getCommandLine().hasOption(Arg.HELP_LICENSES.option())) { new Licenses(createConfiguration(argumentContext), new PrintWriter(System.out, false, StandardCharsets.UTF_8)).printHelp(); return null; } @@ -184,8 +178,8 @@ public static ReportConfiguration createConfiguration(final ArgumentContext argu Optional dirOpt = CLIOptionCollection.INSTANCE.getSelected(Arg.DIR); if (dirOpt.isPresent()) { try { - configuration.addSource(getReportable(commandLine.getParsedOptionValue( - dirOpt.get()), configuration)); + DocumentName directoryName = commandLine.getParsedOptionValue(dirOpt.get()); + configuration.addSource(getReportable(directoryName.asFile(), configuration)); } catch (ParseException e) { throw new ConfigurationException("Unable to set parse " + dirOpt.get(), e); } diff --git a/apache-rat-core/src/main/java/org/apache/rat/OptionCollectionParser.java b/apache-rat-core/src/main/java/org/apache/rat/OptionCollectionParser.java index 71bef61ae..647d03f1c 100644 --- a/apache-rat-core/src/main/java/org/apache/rat/OptionCollectionParser.java +++ b/apache-rat-core/src/main/java/org/apache/rat/OptionCollectionParser.java @@ -32,6 +32,7 @@ import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.apache.commons.lang3.StringUtils; +import org.apache.rat.api.RatException; import org.apache.rat.commandline.Arg; import org.apache.rat.commandline.ArgumentContext; import org.apache.rat.help.Licenses; @@ -62,11 +63,10 @@ public OptionCollectionParser(final UIOptionCollection> optionCollection) { * @param workingDirectory The directory to resolve relative file names against. * @param args the arguments to parse * @return the ArgumentContext for the process. - * @throws IOException on error. - * @throws ParseException on option parsing error. + * @throws RatException on error. */ public ArgumentContext parseCommands(final File workingDirectory, final String[] args) - throws IOException, ParseException { + throws RatException { return parseCommands(workingDirectory, args, uiOptionCollection.getOptions()); } @@ -77,8 +77,7 @@ public ArgumentContext parseCommands(final File workingDirectory, final String[] * @return the CommandLine * @throws ParseException on option parsing error. */ - //@VisibleForTesting - CommandLine parseCommandLine(final Options opts, final String[] args) throws ParseException { + public static CommandLine parseCommandLine(final Options opts, final String[] args) throws ParseException { try { return DefaultParser.builder().setDeprecatedHandler(DeprecationReporter.getLogReporter()) .setAllowPartialMatching(true).build().parse(opts, args); @@ -89,6 +88,15 @@ CommandLine parseCommandLine(final Options opts, final String[] args) throws Par } } + private void printHelp(final ArgumentContext argumentContext) throws RatException { + try { + new Licenses(argumentContext.getConfiguration(), + new PrintWriter(argumentContext.getConfiguration().getOutput().get(), + false, StandardCharsets.UTF_8)).printHelp(); + } catch (IOException e) { + throw new RatException("Unable to print help: " + e.getMessage(), e); + } + } /** * Parses the standard options to create a ReportConfiguration. * @@ -96,22 +104,21 @@ CommandLine parseCommandLine(final Options opts, final String[] args) throws Par * @param args the arguments to parse. * @param options An Options object containing Apache command line options. * @return the ArgumentContext for the process. - * @throws IOException on error. - * @throws ParseException on option parsing error. + * @throws RatException on error. */ private ArgumentContext parseCommands(final File workingDirectory, final String[] args, - final Options options) throws IOException, ParseException { - CommandLine commandLine = parseCommandLine(options, args); - ArgumentContext argumentContext = new ArgumentContext(workingDirectory, commandLine); - Arg.processLogLevel(argumentContext, uiOptionCollection); - populateConfiguration(argumentContext); - if (uiOptionCollection.isSelected(Arg.HELP_LICENSES)) { - new Licenses(argumentContext.getConfiguration(), - new PrintWriter(argumentContext.getConfiguration().getOutput().get(), - false, StandardCharsets.UTF_8)).printHelp(); + final Options options) throws RatException { + try { + ArgumentContext argumentContext = new ArgumentContext(workingDirectory, options, args); + Arg.processLogLevel(argumentContext, uiOptionCollection); + populateConfiguration(argumentContext); + if (uiOptionCollection.isSelected(Arg.HELP_LICENSES)) { + printHelp(argumentContext); + } + return argumentContext; + } catch (ParseException e) { + throw new RatException("Unable to parse command line: " + e.getMessage(), e); } - - return argumentContext; } /** diff --git a/apache-rat-core/src/main/java/org/apache/rat/Report.java b/apache-rat-core/src/main/java/org/apache/rat/Report.java index 5f5f4cb35..06057980b 100644 --- a/apache-rat-core/src/main/java/org/apache/rat/Report.java +++ b/apache-rat-core/src/main/java/org/apache/rat/Report.java @@ -19,8 +19,15 @@ package org.apache.rat; import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; import org.apache.commons.cli.Options; +import org.apache.commons.io.function.IOSupplier; +import org.apache.rat.api.RatException; +import org.apache.rat.commandline.ArgumentContext; import org.apache.rat.document.RatDocumentAnalysisException; import org.apache.rat.help.Help; import org.apache.rat.utils.DefaultLog; @@ -48,28 +55,59 @@ public static void main(final String[] args) throws Exception { System.exit(0); } - ReportConfiguration configuration = OptionCollection.parseCommands(new File("."), args, Report::printUsage); - if (configuration != null) { - configuration.validate(DefaultLog.getInstance()::error); - Reporter reporter = new Reporter(configuration); - reporter.output(); - reporter.writeSummary(DefaultLog.getInstance().asWriter()); + Reporter.Output result = generateReport(CLIOptionCollection.INSTANCE, new File("."), args); + if (result != null) { + result.writeSummary(DefaultLog.getInstance().asWriter()); - if (configuration.getClaimValidator().hasErrors()) { - configuration.getClaimValidator().logIssues(reporter.getClaimsStatistic()); + if (result.getConfiguration().getClaimValidator().hasErrors()) { + result.getConfiguration().getClaimValidator().logIssues(result.getStatistic()); throw new RatDocumentAnalysisException(format("Issues with %s", String.join(", ", - configuration.getClaimValidator().listIssues(reporter.getClaimsStatistic())))); + result.getConfiguration().getClaimValidator().listIssues(result.getStatistic())))); } } } /** - * Prints the usage message on {@code System.out}. - * @param opts The defined options. + * Prints the usage message on the specified output stream. + * @param out The OutputStream supplier + */ + private static void printUsage(final Options options, final IOSupplier out) { + try (OutputStream stream = out.get(); + PrintWriter writer = new PrintWriter(stream, false, StandardCharsets.UTF_8)) { + new Help(writer).printUsage(options); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Generates the report + * @param workingDirectory the directory that we are executing in + * @param args the arguments from the command line. + * @return The Client output. + * @throws Exception on error. */ - private static void printUsage(final Options opts) { - new Help(System.out).printUsage(opts); + static Reporter.Output generateReport(final CLIOptionCollection optionCollection, final File workingDirectory, final String[] args) throws RatException { + Reporter.Output output = null; + OptionCollectionParser optionParser = new OptionCollectionParser(optionCollection); + ArgumentContext argumentContext = optionParser.parseCommands(workingDirectory, args); + ReportConfiguration configuration = argumentContext.getConfiguration(); + if (configuration != null) { + if (argumentContext.getCommandLine().hasOption(CLIOptionCollection.HELP)) { + printUsage(optionCollection.getOptions(), argumentContext.getConfiguration().getOutput()); + } else if (!configuration.hasSource()) { + String msg = "No directories or files specified for scanning. Did you forget to close a multi-argument option?"; + DefaultLog.getInstance().error(msg); + printUsage(optionCollection.getOptions(), argumentContext.getConfiguration().getOutput()); + } else { + configuration.validate(); + Reporter reporter = new Reporter(configuration); + output = reporter.execute(); + output.format(configuration); + } + } + return output; } private Report() { diff --git a/apache-rat-core/src/main/java/org/apache/rat/ReportConfiguration.java b/apache-rat-core/src/main/java/org/apache/rat/ReportConfiguration.java index 579095824..901f170c3 100644 --- a/apache-rat-core/src/main/java/org/apache/rat/ReportConfiguration.java +++ b/apache-rat-core/src/main/java/org/apache/rat/ReportConfiguration.java @@ -35,19 +35,25 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.SortedSet; -import java.util.function.Consumer; +import java.util.stream.Stream; + +import javax.xml.parsers.DocumentBuilder; import org.apache.commons.collections4.set.UnmodifiableSortedSet; import org.apache.commons.io.function.IOSupplier; import org.apache.commons.io.output.CloseShieldOutputStream; +import org.apache.commons.lang3.StringUtils; import org.apache.rat.analysis.IHeaderMatcher; +import org.apache.rat.api.RatException; import org.apache.rat.commandline.StyleSheets; import org.apache.rat.config.AddLicenseHeaders; import org.apache.rat.config.exclusion.ExclusionProcessor; import org.apache.rat.config.exclusion.StandardCollection; import org.apache.rat.config.results.ClaimValidator; +import org.apache.rat.configuration.XMLConfigurationReader; import org.apache.rat.configuration.builders.AnyBuilder; import org.apache.rat.document.DocumentName; import org.apache.rat.document.DocumentNameMatcher; @@ -56,12 +62,20 @@ import org.apache.rat.license.ILicenseFamily; import org.apache.rat.license.LicenseSetFactory; import org.apache.rat.license.LicenseSetFactory.LicenseFilter; +import org.apache.rat.report.RatReport; import org.apache.rat.report.Reportable; +import org.apache.rat.report.claim.ClaimStatistic; +import org.apache.rat.report.xml.writer.XmlWriter; import org.apache.rat.utils.DefaultLog; import org.apache.rat.utils.Log.Level; import org.apache.rat.utils.ReportingSet; +import org.apache.rat.utils.StandardXmlFactory; import org.apache.rat.walker.FileListWalker; import org.apache.rat.walker.ReportableListWalker; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * A configuration object is used by the front end to invoke the @@ -140,7 +154,7 @@ public String desc() { private final List reportables; /** - * A predicate to test if a path should be included in the processing. + * The exclusion processor that determines if a file is included or excluded. */ private final ExclusionProcessor exclusionProcessor; @@ -183,6 +197,10 @@ public ReportConfiguration() { reportables = new ArrayList<>(); } + public Serde serde() { + return new Serde(); + } + /** * Report the excluded files to the appendable object. * @param appendable the appendable object to write to. @@ -242,6 +260,16 @@ public ReportableListWalker.Builder getSources() { return builder; } + // for testing access + Iterable sources() { + return sources; + } + + // for testing access + Stream reportables() { + return reportables.stream().map(Reportable::name); + } + /** * Gets the matcher that matches generated text. * @return the matcher that matches generated text. @@ -252,7 +280,7 @@ public IHeaderMatcher getGeneratedMatcher() { /** * Retrieves the archive processing type. - * @return the archive processing type. + * @return The archive processing type. */ public Processing getArchiveProcessing() { return archiveProcessing == null ? Defaults.ARCHIVE_PROCESSING : archiveProcessing; @@ -268,7 +296,7 @@ public void setArchiveProcessing(final Processing archiveProcessing) { /** * Retrieves the archive processing type. - * @return the archive processing type. + * @return The archive processing type. */ public Processing getStandardProcessing() { return standardProcessing == null ? Defaults.STANDARD_PROCESSING : standardProcessing; @@ -285,7 +313,7 @@ public void setStandardProcessing(final Processing standardProcessing) { /** * Set the log level for reporting collisions in the set of license families. * NOTE: should be set before licenses or license families are added. - * @param level the log level to use. + * @param level The log level to use. */ public void logFamilyCollisions(final Level level) { licenseSetFactory.logFamilyCollisions(level); @@ -293,7 +321,7 @@ public void logFamilyCollisions(final Level level) { /** * Sets the reporting option for duplicate license families. - * @param state the ReportingSet.Option to use for reporting. + * @param state The ReportingSet.Option to use for reporting. */ public void familyDuplicateOption(final ReportingSet.Options state) { licenseSetFactory.familyDuplicateOption(state); @@ -301,7 +329,7 @@ public void familyDuplicateOption(final ReportingSet.Options state) { /** * Sets the log level for reporting license collisions. - * @param level the log level. + * @param level The log level. */ public void logLicenseCollisions(final Level level) { licenseSetFactory.logLicenseCollisions(level); @@ -424,6 +452,14 @@ public void addIncludedFilter(final FileFilter fileFilter) { exclusionProcessor.addIncludedMatcher(new DocumentNameMatcher(fileFilter)); } + /** + * Includes files that match a DocumentNameMatcher. + * @param matcher the DocumentNameMatcher to match. + */ + public void addIncludedMatcher(final DocumentNameMatcher matcher) { + exclusionProcessor.addIncludedMatcher(matcher); + } + /** * Add file patterns that are to be included. These patterns override any exclusion of * the same files. @@ -442,6 +478,11 @@ public DocumentNameMatcher getDocumentExcluder(final DocumentName baseDir) { return exclusionProcessor.getNameMatcher(baseDir); } + // visible for testing. + ExclusionProcessor getExclusionProcessor() { + return exclusionProcessor; + } + /** * Gets the IOSupplier with the style sheet. * @return the Supplier of the InputStream that is the XSLT style sheet to style @@ -478,7 +519,7 @@ public void setStyleSheet(final IODescriptor styleSheet) { */ public void setFrom(final Defaults defaults) { licenseSetFactory.add(defaults.getLicenseSetFactory()); - if (getStyleSheet() == null) { + if (getStyleSheetDescriptor() == null) { setStyleSheet(StyleSheets.PLAIN.getStyleSheet()); } defaults.getStandardExclusion().forEach(this::addExcludedCollection); @@ -852,19 +893,183 @@ public LicenseSetFactory getLicenseSetFactory() { /** * Validates that the configuration is valid. - * @param logger String consumer to log warning messages to. * @throws ConfigurationException on configuration error. */ - public void validate(final Consumer logger) { + public void validate() { if (!hasSource()) { String msg = "At least one source must be specified"; - logger.accept(msg); + DefaultLog.getInstance().error(msg); throw new ConfigurationException(msg); } - if (licenseSetFactory.getLicenses(LicenseFilter.ALL).isEmpty()) { - String msg = "You must specify at least one license"; - logger.accept(msg); - throw new ConfigurationException(msg); + licenseSetFactory.validate(); + } + + /** + * An IODescriptor comprises a name and an IOSupplier. The name should identify the contents of the stream. + * @param name the name of the supplier. + * @param ioSupplier the IOSupplier that provides either an InputStream or an OutputStream + * @param either InputStream or OutputStream. + */ + public record IODescriptor(String name, IOSupplier ioSupplier) { + + // OUTPUT CONSTRUCTORS + + /** + * Creates an output IODescriptor for the file name within the working directory. + * @param name the name of the file to open. + * @param workingDirectory the working directory for the file. + * @return the Output IODescriptor. + */ + static IODescriptor output(final String name, final DocumentName workingDirectory) { + DocumentName docName = workingDirectory.resolve(name); + return new IODescriptor<>(name, () -> new FileOutputStream(docName.asFile())); + } + + /** + * Creates an output IODescriptor for the file. Does not modify for working directory.. + * @param file the file to open. + * @return the Output IODescriptor. + */ + static IODescriptor output(final File file) { + return new IODescriptor<>(file.toString(), () -> new FileOutputStream(file, true)); + } + + // INPUT CONSTRUCTORS + + /** + * Creates an input IODescriptor for the file. Does not modify for working directory. + * @param file the file to open. + * @return the Input IODescriptor. + */ + static IODescriptor input(final File file) { + return new IODescriptor<>(file.toString(), () -> new FileInputStream(file)); + } + } + + /** + * Serializes the ReportConfiguration into an XML document that can be deserialzed by the Serde. + * Deserialized ReportConfigurations can not be executed as the reportable objects a simply named placeholders + * and do not have access to the original object. + */ + public class Serde { + /** + * Writes the configuration as an XML document to the appendable. + * + * @param appendable the Appendable to write to. + * @throws IOException on error. + */ + public void serialize(final Appendable appendable) throws IOException { + try (XmlWriter writer = new XmlWriter(appendable)) { + writer.startElement("ReportConfiguration") + .attribute("addingLicenses", Boolean.toString(addingLicenses)) + .attribute("addingLicensesForced", Boolean.toString(addingLicensesForced)) + .attribute("listFamilies", listFamilies.name()) + .attribute("listLicenses", listLicenses.name()) + .attribute("dryRun", Boolean.toString(dryRun)) + .attribute("archiveProcessing", getArchiveProcessing().name()) + .attribute("standardProcessing", getStandardProcessing().name()) + .attribute("stylesheet", styleSheet.name()) + .attribute("output", out.name()); + if (StringUtils.isNotEmpty(copyrightMessage)) { + writer.startElement("copyrightMessage").content(copyrightMessage).closeElement(); + } + writer.startElement("sources"); + for (File f : sources) { + writer.startElement("source").attribute("name", f.toString()).closeElement(); + } + writer.closeElement("sources").startElement("reportables"); + for (Reportable reportable : reportables) { + writer.startElement("reportable") + .attribute("baseName", reportable.name().getBaseName()) + .attribute("name", reportable.name().toString()) + .attribute("class", reportable.getClass().getName()).closeElement(); + } + writer.closeElement(); + + exclusionProcessor.serde().serialize(writer); + + writer.startElement("claimValidator"); + for (ClaimStatistic.Counter counter : ClaimStatistic.Counter.values()) { + writer.startElement("claimCounter") + .attribute("name", counter.name()).attribute("min", Integer.toString(claimValidator.getMin(counter))) + .attribute("max", Integer.toString(claimValidator.getMax(counter))).closeElement(); + } + writer.closeElement(); + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new IOException(e); + } + } + + public void deserialize(final IOSupplier inputStreamSupplier, final DocumentName workingDirectory) throws IOException { + DocumentBuilder builder = StandardXmlFactory.documentBuilder(); + org.w3c.dom.Document document; + try (InputStream stream = inputStreamSupplier.get()) { + document = builder.parse(stream); + } catch (SAXException e) { + throw new IOException("Unable to read input", e); + } + Node node = document.getDocumentElement(); + if (!node.getNodeName().equals("ReportConfiguration")) { + throw new IOException("Invalid ReportConfiguration"); + } + Map attributes = XMLConfigurationReader.attributes(node); + addingLicenses = Boolean.parseBoolean(attributes.get("addingLicenses")); + addingLicensesForced = Boolean.parseBoolean(attributes.get("addingLicensesForced")); + listFamilies = LicenseFilter.valueOf(attributes.get("listFamilies")); + listLicenses = LicenseFilter.valueOf(attributes.get("listLicenses")); + dryRun = Boolean.parseBoolean(attributes.get("dryRun")); + archiveProcessing = Processing.valueOf(attributes.get("archiveProcessing")); + standardProcessing = Processing.valueOf(attributes.get("standardProcessing")); + String styleName = attributes.get("stylesheet"); + if (styleName != null) { + styleSheet = StyleSheets.getStyleSheet(styleName, workingDirectory); + } + String outputName = attributes.get("output"); + if (outputName != null) { + if (outputName.equals(ReportConfiguration.SYSTEM_OUT.name())) { + out = ReportConfiguration.SYSTEM_OUT; + } else { + out = IODescriptor.output(outputName, workingDirectory); + } + } + + XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("copyrightMessage"), + lNode -> setCopyrightMessage(lNode.getTextContent())); + + XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("source"), lNode -> { + Map nAttributes = XMLConfigurationReader.attributes(lNode); + addSource(new File(nAttributes.get("name"))); + }); + + // Deserialize the reportables. + XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("reportable"), lNode -> { + Map nAttributes = XMLConfigurationReader.attributes(lNode); + DocumentName documentName = DocumentName.builder().setBaseName(nAttributes.get("baseName")) + .setName(nAttributes.get("name")).build(); + addSource(new DeserializedReportable(documentName)); + }); + + exclusionProcessor.serde().deserialize(document.getElementsByTagName("ExclusionProcessor").item(0)); + + XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("claimCounter"), lNode -> { + Map nAttributes = XMLConfigurationReader.attributes(lNode); + ClaimStatistic.Counter counter = ClaimStatistic.Counter.valueOf(nAttributes.get("name")); + claimValidator.setMin(counter, Integer.parseInt(nAttributes.get("min"))); + claimValidator.setMax(counter, Integer.parseInt(nAttributes.get("max"))); + }); + } + } + + /** + * A record that identifies a deserialized reportable. Deserialized reportables are not executable. + * @param name the name of the reportable. + */ + private record DeserializedReportable(DocumentName name) implements Reportable { + @Override + public void run(final RatReport report) throws RatException { + throw new RatException("Attempt to run a deserialized reportable"); } } @@ -907,4 +1112,131 @@ static IODescriptor input(final File file) { return new IODescriptor<>(file.toString(), () -> new FileInputStream(file)); } } + + /** + * Serializes the ReportConfiguration into an XML document that can be deserialzed by the Serde. + * Deserialized ReportConfigurations can not be executed as the reportable objects a simply named placeholders + * and do not have access to the original object. + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public class Serde { + /** + * Writes the configuration as an XML document to the appendable. + * + * @param appendable the Appendable to write to. + * @throws IOException on error. + */ + public void serialize(final Appendable appendable) throws IOException { + try (XmlWriter writer = new XmlWriter(appendable)) { + writer.startElement("ReportConfiguration") + .attribute("addingLicenses", Boolean.toString(addingLicenses)) + .attribute("addingLicensesForced", Boolean.toString(addingLicensesForced)) + .attribute("listFamilies", listFamilies.name()) + .attribute("listLicenses", listLicenses.name()) + .attribute("dryRun", Boolean.toString(dryRun)) + .attribute("archiveProcessing", getArchiveProcessing().name()) + .attribute("standardProcessing", getStandardProcessing().name()) + .attribute("stylesheet", styleSheet.name()) + .attribute("output", out.name()); + if (StringUtils.isNotEmpty(copyrightMessage)) { + writer.startElement("copyrightMessage").content(copyrightMessage).closeElement(); + } + writer.startElement("sources"); + for (File f : sources) { + writer.startElement("source").attribute("name", f.toString()).closeElement(); + } + writer.closeElement("sources").startElement("reportables"); + for (Reportable reportable : reportables) { + writer.startElement("reportable") + .attribute("baseName", reportable.name().getBaseName()) + .attribute("name", reportable.name().toString()) + .attribute("class", reportable.getClass().getName()).closeElement(); + } + writer.closeElement(); + + exclusionProcessor.serde().serialize(writer); + + writer.startElement("claimValidator"); + for (ClaimStatistic.Counter counter : ClaimStatistic.Counter.values()) { + writer.startElement("claimCounter") + .attribute("name", counter.name()).attribute("min", Integer.toString(claimValidator.getMin(counter))) + .attribute("max", Integer.toString(claimValidator.getMax(counter))).closeElement(); + } + writer.closeElement(); + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new IOException(e); + } + } + + public void deserialize(final IOSupplier inputStreamSupplier, final DocumentName workingDirectory) throws IOException { + org.w3c.dom.Document document; + try (InputStream stream = inputStreamSupplier.get()) { + document = StandardXmlFactory.documentBuilder().parse(stream); + } catch (SAXException e) { + throw new IOException("Unable to read input", e); + } + Node node = document.getDocumentElement(); + if (!node.getNodeName().equals("ReportConfiguration")) { + throw new IOException("Invalid ReportConfiguration"); + } + Map attributes = XMLConfigurationReader.attributes(node); + addingLicenses = Boolean.parseBoolean(attributes.get("addingLicenses")); + addingLicensesForced = Boolean.parseBoolean(attributes.get("addingLicensesForced")); + listFamilies = LicenseFilter.valueOf(attributes.get("listFamilies")); + listLicenses = LicenseFilter.valueOf(attributes.get("listLicenses")); + dryRun = Boolean.parseBoolean(attributes.get("dryRun")); + archiveProcessing = Processing.valueOf(attributes.get("archiveProcessing")); + standardProcessing = Processing.valueOf(attributes.get("standardProcessing")); + String styleName = attributes.get("stylesheet"); + if (styleName != null) { + styleSheet = StyleSheets.getStyleSheet(styleName); + } + String outputName = attributes.get("output"); + if (outputName != null) { + if (outputName.equals(ReportConfiguration.SYSTEM_OUT.name())) { + out = ReportConfiguration.SYSTEM_OUT; + } else { + out = IODescriptor.output(outputName, workingDirectory); + } + } + + XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("copyrightMessage"), + lNode -> setCopyrightMessage(lNode.getTextContent())); + + XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("source"), lNode -> { + Map nAttributes = XMLConfigurationReader.attributes(lNode); + addSource(new File(nAttributes.get("name"))); + }); + + // Deserialize the reportables. + XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("reportable"), lNode -> { + Map nAttributes = XMLConfigurationReader.attributes(lNode); + DocumentName documentName = DocumentName.builder().setBaseName(nAttributes.get("baseName")) + .setName(nAttributes.get("name")).build(); + addSource(new DeserializedReportable(documentName)); + }); + + exclusionProcessor.serde().deserialize(document.getElementsByTagName("ExclusionProcessor").item(0)); + + XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("claimCounter"), lNode -> { + Map nAttributes = XMLConfigurationReader.attributes(lNode); + ClaimStatistic.Counter counter = ClaimStatistic.Counter.valueOf(nAttributes.get("name")); + claimValidator.setMin(counter, Integer.parseInt(nAttributes.get("min"))); + claimValidator.setMax(counter, Integer.parseInt(nAttributes.get("max"))); + }); + } + } + + /** + * A record that identifies a deserialized reportable. Deserialized reportables are not executable. + * @param name the name of the reportable. + */ + private record DeserializedReportable(DocumentName name) implements Reportable { + @Override + public void run(final RatReport report) throws RatException { + throw new RatException("Attempt to run a deserialized reportable"); + } + } } diff --git a/apache-rat-core/src/main/java/org/apache/rat/Reporter.java b/apache-rat-core/src/main/java/org/apache/rat/Reporter.java index 5cf396d9c..8ef1836dc 100644 --- a/apache-rat-core/src/main/java/org/apache/rat/Reporter.java +++ b/apache-rat-core/src/main/java/org/apache/rat/Reporter.java @@ -18,14 +18,14 @@ */ package org.apache.rat; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; -import java.io.Writer; +import java.io.StringReader; import java.nio.charset.StandardCharsets; import javax.xml.transform.TransformerException; @@ -34,6 +34,7 @@ import org.apache.commons.io.function.IOSupplier; import org.apache.rat.api.RatException; +import org.apache.rat.document.DocumentName; import org.apache.rat.license.LicenseSetFactory.LicenseFilter; import org.apache.rat.report.RatReport; import org.apache.rat.report.claim.ClaimStatistic; @@ -41,6 +42,8 @@ import org.apache.rat.report.xml.writer.XmlWriter; import org.apache.rat.utils.StandardXmlFactory; import org.w3c.dom.Document; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; /** * Class that executes the report as defined in a {@link ReportConfiguration} and stores @@ -48,18 +51,21 @@ */ public class Reporter { - /** Format used for listing licenses. */ + /** + * Format used for listing licenses. + */ private static final String LICENSE_FORMAT = "%s:\t%s%n\t\t%s%n"; - /** The XML output document */ - private Document document; - - /** Statistics generated as the report was built */ - private ClaimStatistic statistic; - - /** The configuration for the report */ + /** + * The configuration for the report + */ private final ReportConfiguration configuration; + /** + * The output from the execution. + */ + private Output output; + /** * Create the reporter. * @@ -71,79 +77,49 @@ public Reporter(final ReportConfiguration configuration) { /** * Executes the report and builds the output. - * This method will build the internal XML document if it does not already exist. - * If this method or either of the {@link #output()} methods have already been called this method will return - * the previous results. - * @return the claim statistics. + * + * @return the Output object. * @throws RatException on error. */ - public ClaimStatistic execute() throws RatException { - if (document == null || statistic == null) { - try { - if (configuration.hasSource()) { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - Writer outputWriter = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); - try (XmlWriter writer = new XmlWriter(outputWriter)) { - statistic = new ClaimStatistic(); - RatReport report = XmlReportFactory.createStandardReport(writer, statistic, configuration); - report.startReport(); - configuration.getSources().build().run(report); - report.endReport(); - } - InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); - document = StandardXmlFactory.documentBuilder().parse(inputStream); - } else { - document = StandardXmlFactory.documentBuilder().newDocument(); - statistic = new ClaimStatistic(); + public Output execute() throws RatException { + try { + Output.Builder builder = Output.builder().configuration(configuration); + if (configuration.hasSource()) { + StringBuilder sb = new StringBuilder(); + try (XmlWriter writer = new XmlWriter(sb)) { + writer.startDocument(); + ClaimStatistic statistic = new ClaimStatistic(); + builder.statistic(statistic); + RatReport report = XmlReportFactory.createStandardReport(writer, statistic, configuration); + report.startReport(); + configuration.getSources().build().run(report); + report.endReport(); + InputSource inputSource = new InputSource(new StringReader(sb.toString())); + builder.document(StandardXmlFactory.documentBuilder().parse(inputSource)); } - } catch (Exception e) { - throw RatException.makeRatException(e); + } else { + builder.document = StandardXmlFactory.documentBuilder().newDocument(); + builder.statistic(new ClaimStatistic()); } + this.output = builder.build(); + return output; + } catch (Exception e) { + throw RatException.makeRatException(e); } - return statistic; } /** - * Get the claim statistics from the run. + * Gets the output from the last {@link #execute} call or {@code null} if {@link #execute} has not been called. * - * @return the claim statistics. + * @return the output */ - public ClaimStatistic getClaimsStatistic() { - return statistic; - } - - /** - * Outputs the report using the stylesheet and output specified in the configuration. - * @return the Claim statistic from the run. - * @throws RatException on error. - */ - public ClaimStatistic output() throws RatException { - return output(configuration.getStyleSheet(), configuration.getOutput()); - } - - /** - * Outputs the report to the specified output using the stylesheet. It is safe to call this method more than once - * in order to generate multiple reports from the same run. - * - * @param stylesheet the style sheet to use for XSLT formatting. - * @param output the output stream to write to. - * @return the Claim statistic for the run. - * @throws RatException on error. - */ - public ClaimStatistic output(final IOSupplier stylesheet, final IOSupplier output) throws RatException { - ClaimStatistic result = execute(); - try (OutputStream out = output.get(); - InputStream styleIn = stylesheet.get()) { - StandardXmlFactory.createTransformer(styleIn).transform(new DOMSource(document), - new StreamResult(new OutputStreamWriter(out, StandardCharsets.UTF_8))); - return result; - } catch (TransformerException | IOException e) { - throw new RatException(e); - } + public Output getOutput() { + return output; } /** * Lists the licenses on the configured output stream. + * * @param configuration The configuration for the system * @param filter the license filter that specifies which licenses to output. * @throws IOException if PrintWriter can not be retrieved from configuration. @@ -159,24 +135,197 @@ public static void listLicenses(final ReportConfiguration configuration, final L } /** - * Writes a text summary of issues with the run. - * @param appendable the appendable to write to. - * @throws IOException on error. + * The output from a report run. */ - public void writeSummary(final Appendable appendable) throws IOException { - appendable.append("RAT summary:").append(System.lineSeparator()); - for (ClaimStatistic.Counter counter : ClaimStatistic.Counter.values()) { - appendable.append(" ").append(counter.displayName()).append(": ") - .append(Integer.toString(getClaimsStatistic().getCounter(counter))) - .append(System.lineSeparator()); + public static final class Output { + /** + * The XML output document + */ + private final Document document; + /** + * The claim statics from the execution that generated the document. + * May be empty if the Document was read from disk. + */ + private final ClaimStatistic statistic; + /** + * The configuration that generated the document + */ + private final ReportConfiguration configuration; + + /** + * Create an output with statistics. + * + * @param builder the Builder + */ + private Output(final Builder builder) { + this.document = builder.document; + this.statistic = builder.statistic == null ? new ClaimStatistic() : builder.statistic; + this.configuration = builder.configuration == null ? new ReportConfiguration() : builder.configuration; } - } - /** - * Gets the document that was generated during execution. - * @return the document that was generated during execution. - */ - public Document getDocument() { - return document; + public static Builder builder() { + return new Builder(); + } + + /** + * Gets the document that was generated during execution. + * + * @return the document that was generated during execution. + */ + public Document getDocument() { + return document; + } + + /** + * Get the claim statistics from the run. + * + * @return the claim statistics. + */ + public ClaimStatistic getStatistic() { + return statistic; + } + + public ReportConfiguration getConfiguration() { + return configuration; + } + + /** + * Formats the report to the output and using the stylesheet found in the report configuration. + * + * @param config s RAT report configuration. + * @throws RatException on error. + */ + public void format(final ReportConfiguration config) throws RatException { + format(config.getStyleSheet(), config.getOutput()); + } + + /** + * Formats the report to the specified output using the stylesheet. It is safe to call this method more than once + * in order to generate multiple reports from the same run. + * + * @param stylesheet the style sheet to use for XSLT formatting. + * @param output the output stream to write to. + * @throws RatException on error. + */ + public void format(final IOSupplier stylesheet, final IOSupplier output) throws RatException { + + try (OutputStream out = output.get(); + InputStream styleIn = stylesheet.get()) { + StandardXmlFactory.createTransformer(styleIn).transform(new DOMSource(document), + new StreamResult(new OutputStreamWriter(out, StandardCharsets.UTF_8))); + } catch (TransformerException | IOException e) { + throw new RatException(e); + } + } + + /** + * Lists the licenses on the print writer. + * + * @param printWriter The print writer to write to. + * @param filter the license filter that specifies which licenses to output. + */ + public void listLicenses(final PrintWriter printWriter, final LicenseFilter filter) { + printWriter.format("Licenses (%s):%n", filter); + configuration.getLicenses(filter) + .forEach(lic -> printWriter.format(LICENSE_FORMAT, lic.getLicenseFamily().getFamilyCategory(), + lic.getLicenseFamily().getFamilyName(), lic.getNote())); + printWriter.println(); + } + + /** + * Lists the licenses on the output specified in the configuration. + * + * @param filter the license filter that specifies which licenses to output. + * @throws IOException if PrintWriter can not be retrieved from configuration. + */ + public void listLicenses(final LicenseFilter filter) throws IOException { + try (PrintWriter pw = configuration.getWriter().get()) { + listLicenses(pw, filter); + } + } + + /** + * Writes a text summary of issues with the run. + * + * @param appendable the appendable to write to. + * @throws IOException on error. + */ + public void writeSummary(final Appendable appendable) throws IOException { + appendable.append("RAT summary:").append(System.lineSeparator()); + for (ClaimStatistic.Counter counter : ClaimStatistic.Counter.values()) { + appendable.append(" ").append(counter.displayName()).append(": ") + .append(Integer.toString(statistic.getCounter(counter))) + .append(System.lineSeparator()); + } + } + + public static final class Builder { + /** + * The document that was generated + */ + private Document document; + /** + * The claim statistic from the execution that generated the document. + * May be empty if the Document was read from disk. + */ + private ClaimStatistic statistic; + /** + * The configuration that generated the document + */ + private ReportConfiguration configuration; + + public Builder document(final Document document) { + this.document = document; + return this; + } + + public Builder document(final String fileName, final DocumentName workingDirectory) { + File inputFile = workingDirectory.resolve(fileName).asFile(); + try (InputStream inputStream = new FileInputStream(inputFile)) { + this.document = StandardXmlFactory.documentBuilder().parse(inputStream); + } catch (SAXException | IOException e) { + throw new ConfigurationException("Unable to read file: " + inputFile, e); + } + return this; + } + + public Output build() { + return new Output(this); + } + + public Builder statistic(final ClaimStatistic statistic) { + this.statistic = statistic; + return this; + } + + public Builder statistic(final String fileName, final DocumentName workingDirectory) { + File sourceFile = workingDirectory.resolve(fileName).asFile(); + try { + ClaimStatistic newStatistic = new ClaimStatistic(); + newStatistic.serde().deserialize(() -> new FileInputStream(sourceFile)); + this.statistic = newStatistic; + return this; + } catch (IOException e) { + throw new ConfigurationException("Unable to read file: " + sourceFile, e); + } + } + + public Builder configuration(final ReportConfiguration configuration) { + this.configuration = configuration; + return this; + } + + public Builder configuration(final String fileName, final DocumentName workingDirectory) { + File configurationFile = workingDirectory.resolve(fileName).asFile(); + try { + ReportConfiguration config = new ReportConfiguration(); + config.serde().deserialize(() -> new FileInputStream(configurationFile), workingDirectory); + this.configuration = config; + return this; + } catch (IOException e) { + throw new ConfigurationException("Unable to read file: " + configurationFile, e); + } + } + } } } diff --git a/apache-rat-core/src/main/java/org/apache/rat/commandline/Arg.java b/apache-rat-core/src/main/java/org/apache/rat/commandline/Arg.java index d11e90560..dc9911cf0 100644 --- a/apache-rat-core/src/main/java/org/apache/rat/commandline/Arg.java +++ b/apache-rat-core/src/main/java/org/apache/rat/commandline/Arg.java @@ -218,7 +218,7 @@ public enum Arg { * Option to read a file licenses to be removed from the approved list. */ LICENSES_DENIED_FILE(new OptionGroup().addOption(Option.builder().longOpt("licenses-denied-file") - .hasArg().argName("File").type(File.class) + .hasArg().argName("File").type(DocumentName.class) .converter(Converters.FILE_CONVERTER) .desc("Name of file containing comma separated lists of the denied license IDs. " + "These licenses will be removed from the list of approved licenses. " + @@ -326,14 +326,14 @@ public enum Arg { */ EXCLUDE_FILE(new OptionGroup() .addOption(Option.builder("E").longOpt("exclude-file") - .argName("File").hasArg().type(File.class) + .argName("File").hasArg().type(DocumentName.class) .converter(Converters.FILE_CONVERTER) .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17") .setDescription(StdMsgs.useMsg("--input-exclude-file")).get()) .desc("Reads entries from a file. Entries will be excluded from processing.") .build()) .addOption(Option.builder().longOpt("input-exclude-file") - .argName("File").hasArg().type(File.class) + .argName("File").hasArg().type(DocumentName.class) .converter(Converters.FILE_CONVERTER) .desc("Reads entries from a file. Entries will be excluded from processing.") .build()), @@ -408,12 +408,12 @@ public enum Arg { */ INCLUDE_FILE(new OptionGroup() .addOption(Option.builder().longOpt("input-include-file") - .argName("File").hasArg().type(File.class) + .argName("File").hasArg().type(DocumentName.class) .converter(Converters.FILE_CONVERTER) .desc("Reads entries from a file. Entries will override excluded files.") .build()) .addOption(Option.builder().longOpt("includes-file") - .argName("File").hasArg().type(File.class) + .argName("File").hasArg().type(DocumentName.class) .converter(Converters.FILE_CONVERTER) .desc("Reads entries from a file. Entries will override excluded files.") .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17") @@ -485,7 +485,8 @@ public enum Arg { * Stop processing an input stream and declare an input file. */ DIR(new OptionGroup().addOption(Option.builder().option("d").longOpt("dir").hasArg() - .type(File.class) + .type(DocumentName.class) + .converter(Converters.FILE_CONVERTER) .desc("Used to indicate end of list when using options that take multiple arguments.").argName("DirOrArchive") .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17") .setDescription("Use the standard '--' to signal the end of arguments.").get()).build()), @@ -517,14 +518,14 @@ public enum Arg { if ("x".equals(key)) { // display deprecated message. context.getCommandLine().hasOption("x"); - context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet("xml")); + context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet("xml", context.getWorkingDirectory())); } else { String[] style = context.getCommandLine().getOptionValues(selected); if (style.length != 1) { DefaultLog.getInstance().error("Please specify a single stylesheet"); throw new ConfigurationException("Please specify a single stylesheet"); } - context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet(style[0])); + context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet(style[0], context.getWorkingDirectory())); } }), diff --git a/apache-rat-core/src/main/java/org/apache/rat/commandline/ArgumentContext.java b/apache-rat-core/src/main/java/org/apache/rat/commandline/ArgumentContext.java index d4374a57b..49917397d 100644 --- a/apache-rat-core/src/main/java/org/apache/rat/commandline/ArgumentContext.java +++ b/apache-rat-core/src/main/java/org/apache/rat/commandline/ArgumentContext.java @@ -22,7 +22,9 @@ import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; +import org.apache.rat.OptionCollectionParser; import org.apache.rat.ReportConfiguration; import org.apache.rat.document.DocumentName; import org.apache.rat.ui.UIOptionCollection; @@ -46,21 +48,26 @@ public final class ArgumentContext { * Creates a context with the specified configuration. * @param workingDirectory the directory from which relative file names will be resolved. * @param configuration The configuration that is being built. - * @param commandLine The command line that is building the configuration. + * @param opts the Options for the command line. + * @param args the arguments for the options. + * @throws ParseException if the options can not parse the arguments. */ - public ArgumentContext(final File workingDirectory, final ReportConfiguration configuration, final CommandLine commandLine) { + public ArgumentContext(final File workingDirectory, final ReportConfiguration configuration, final Options opts, final String[] args) + throws ParseException { this.workingDirectory = DocumentName.builder(workingDirectory).build(); - this.commandLine = commandLine; + this.commandLine = OptionCollectionParser.parseCommandLine(opts, args); this.configuration = configuration; } /** * Creates a context with an empty configuration. * @param workingDirectory The directory from which to resolve relative file names. - * @param commandLine The command line. + * @param opts the Options for the command line. + * @param args the arguments for the options. + * @throws ParseException if the options can not parse the arguments. */ - public ArgumentContext(final File workingDirectory, final CommandLine commandLine) { - this(workingDirectory, new ReportConfiguration(), commandLine); + public ArgumentContext(final File workingDirectory, final Options opts, final String[] args) throws ParseException { + this(workingDirectory, new ReportConfiguration(), opts, args); } /** diff --git a/apache-rat-core/src/main/java/org/apache/rat/commandline/StyleSheets.java b/apache-rat-core/src/main/java/org/apache/rat/commandline/StyleSheets.java index b56e0dde7..3837a186c 100644 --- a/apache-rat-core/src/main/java/org/apache/rat/commandline/StyleSheets.java +++ b/apache-rat-core/src/main/java/org/apache/rat/commandline/StyleSheets.java @@ -21,12 +21,11 @@ import java.io.InputStream; import java.net.URL; import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Objects; import org.apache.rat.ConfigurationException; import org.apache.rat.ReportConfiguration; +import org.apache.rat.document.DocumentName; import static java.lang.String.format; @@ -49,8 +48,11 @@ public enum StyleSheets { /** * The pretty-printed XML style sheet. */ - XML("xml", "Produces output in pretty-printed XML."); - + XML("xml", "Produces output in pretty-printed XML."), + /** + * Official HTML5 stylesheet. + */ + XHTML5("xhtml5", "Produces a HTML5 report"); /** * The name of the style sheet. Must map to bundled resource XSLT file */ @@ -71,8 +73,8 @@ public enum StyleSheets { } /** - * Gets the IOSupplier for a style sheet. - * @return an IOSupplier for the sheet. + * Gets the IODescriptor for a style sheet. + * @return an IODescriptor for the sheet. */ public ReportConfiguration.IODescriptor getStyleSheet() { URL url = StyleSheets.class.getClassLoader().getResource(format("org/apache/rat/%s.xsl", name)); @@ -81,20 +83,21 @@ public ReportConfiguration.IODescriptor getStyleSheet() { } /** - * Gets the IOSupplier for a style sheet. + * Gets the IODescriptor for a style sheet. * @param name the short name for or the path to a style sheet. - * @return the IOSupplier for the style sheet. + * @param workingDirectory the working directory to resolve the name against. + * @return the IODescriptor for the style sheet. */ - public static ReportConfiguration.IODescriptor getStyleSheet(final String name) { + public static ReportConfiguration.IODescriptor getStyleSheet(final String name, final DocumentName workingDirectory) { URL url = StyleSheets.class.getClassLoader().getResource(format("org/apache/rat/%s.xsl", name)); if (url != null) { return new ReportConfiguration.IODescriptor<>(name, url::openStream); } - Path p = Paths.get(name); - if (p.toFile().exists()) { - return new ReportConfiguration.IODescriptor<>(name, () -> Files.newInputStream(p)); + DocumentName xslt = workingDirectory.resolve(name); + if (xslt.asFile().exists()) { + return new ReportConfiguration.IODescriptor<>(xslt.toString(), () -> Files.newInputStream(xslt.asFile().toPath())); } - throw new ConfigurationException(format("Stylesheet file '%s' not found", name)); + throw new ConfigurationException(format("Stylesheet file '%s' not found: %s", name, xslt.getName())); } /** diff --git a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/ExclusionProcessor.java b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/ExclusionProcessor.java index d7b5862be..5d9bff23f 100644 --- a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/ExclusionProcessor.java +++ b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/ExclusionProcessor.java @@ -20,17 +20,25 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.TreeSet; +import java.util.function.Predicate; import java.util.stream.Collectors; +import org.apache.commons.lang3.NotImplementedException; +import org.apache.rat.configuration.XMLConfigurationReader; import org.apache.rat.document.DocumentName; import org.apache.rat.document.DocumentNameMatcher; +import org.apache.rat.report.xml.writer.XmlWriter; import org.apache.rat.utils.DefaultLog; import org.apache.rat.utils.ExtendedIterator; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; import static java.lang.String.format; @@ -74,7 +82,51 @@ public ExclusionProcessor() { excludedCollections = new HashSet<>(); } - /** Reset the {@link #lastMatcher} and {@link #lastMatcherBaseDir} to start again */ + public Serde serde() { + return new Serde(); + } + + /* the following set of methods are here for testing purposes */ + + Set getExcludedPatterns() { + return new HashSet<>(excludedPatterns); + } + + Collection getExcludedPaths() { + return new ArrayList<>(excludedPaths); + } + + Set getIncludedPatterns() { + return new HashSet<>(includedPatterns); + } + + Collection getIncludedPaths() { + return new ArrayList<>(includedPaths); + } + + Collection getFileProcessors() { + return new HashSet<>(fileProcessors); + } + + Set getIncludedCollections() { + return new HashSet<>(includedCollections); + } + + Set getExcludedCollections() { + return new HashSet<>(excludedCollections); + } + + DocumentNameMatcher getLastMatcher() { + return lastMatcher; + } + + DocumentName getLastMatcherBaseDir() { + return lastMatcherBaseDir; + } + + /** + * Reset the {@link #lastMatcher} and {@link #lastMatcherBaseDir} to start again + */ private void resetLastMatcher() { lastMatcher = null; lastMatcherBaseDir = null; @@ -86,9 +138,11 @@ private void resetLastMatcher() { * @return this */ public ExclusionProcessor addIncludedPatterns(final Iterable patterns) { - DefaultLog.getInstance().debug(format("Including patterns: %s", String.join(", ", patterns))); - patterns.forEach(includedPatterns::add); - resetLastMatcher(); + if (patterns != null) { + DefaultLog.getInstance().debug(format("Including patterns: %s", String.join(", ", patterns))); + patterns.forEach(includedPatterns::add); + resetLastMatcher(); + } return this; } @@ -139,9 +193,11 @@ public ExclusionProcessor addIncludedCollection(final StandardCollection collect * @return this */ public ExclusionProcessor addExcludedPatterns(final Iterable patterns) { - DefaultLog.getInstance().debug(format("Excluding patterns: %s", String.join(", ", patterns))); - patterns.forEach(excludedPatterns::add); - resetLastMatcher(); + if (patterns != null) { + DefaultLog.getInstance().debug(format("Excluding patterns: %s", String.join(", ", patterns))); + patterns.forEach(excludedPatterns::add); + resetLastMatcher(); + } return this; } @@ -177,9 +233,11 @@ public void reportExclusions(final Appendable appendable) throws IOException { for (DocumentNameMatcher nameMatcher : excludedPaths) { appendable.append(format("Excluding %s.%n", nameMatcher.toString())); } + for (DocumentNameMatcher nameMatcher : includedPaths) { + appendable.append(format("Including %s.%n", nameMatcher.toString())); + } } - /** * Excludes the files/directories specified by a StandardCollection. * @param collection the StandardCollection that identifies the files to exclude. @@ -203,7 +261,7 @@ public ExclusionProcessor addExcludedCollection(final StandardCollection collect public DocumentNameMatcher getNameMatcher(final DocumentName basedir) { // if lastMatcher is not set or the basedir is not the same as the last one then // we have to regenerate the matching document. - // Otherwise we can just return the lastMatcher since there is no change. + // Otherwise, we can just return the lastMatcher since there is no change. if (lastMatcher == null || !basedir.equals(lastMatcherBaseDir)) { lastMatcherBaseDir = basedir; @@ -337,4 +395,91 @@ private void extractPaths(final MatcherSet.Builder matcherBuilder) { } } + /** + * Serializes and deserializes the ExclusionProcessor to XML document + */ + public class Serde { + /** The pattern attribute name */ + private static final String PATTERN = "pattern"; + /** THe name attribute name */ + private static final String NAME = "name"; + + /** + * Serialize the ExclusionProcessor to XML writer. + * @param writer the writer to serialize to. + * @throws IOException on Error + */ + public void serialize(final XmlWriter writer) throws IOException { + writer.startElement("ExclusionProcessor"); + + for (String pattern : excludedPatterns) { + writer.startElement("excludedPattern").attribute(PATTERN, pattern).closeElement(); + } + for (StandardCollection obj : excludedCollections) { + writer.startElement("excludedCollection").attribute(NAME, obj.name()).closeElement(); + } + for (DocumentNameMatcher obj : excludedPaths) { + writer.startElement("excludedPath").attribute(NAME, obj.toString()).closeElement(); + } + + for (String pattern : includedPatterns) { + writer.startElement("includedPattern").attribute(PATTERN, pattern).closeElement(); + } + for (StandardCollection obj : includedCollections) { + writer.startElement("includedCollection").attribute(NAME, obj.name()).closeElement(); + } + for (DocumentNameMatcher obj : includedPaths) { + writer.startElement("includedPath").attribute(NAME, obj.toString()).closeElement(); + } + + for (StandardCollection obj : fileProcessors) { + writer.startElement("fileProcessor").attribute(NAME, obj.name()).closeElement(); + } + writer.closeElement(); + + } + + /** + * Deserialize from XML Document node to ExclusionProcessor + * @param xmlNode the node to deserialize from. + */ + public void deserialize(final Node xmlNode) { + final NodeList children = xmlNode.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + Map attributes = XMLConfigurationReader.attributes(child); + switch (child.getNodeName()) { + case "excludedPattern" -> + excludedPatterns.add(attributes.get(PATTERN)); + + case "excludedCollection" -> + excludedCollections.add(StandardCollection.valueOf(attributes.get(NAME))); + + case "excludedPath" -> + excludedPaths.add(new DocumentNameMatcher(attributes.get(NAME), + (Predicate) x -> { + throw new NotImplementedException("Deserialized ExclusionProcessor can not evaluate paths"); + })); + + case "includedPattern" -> + includedPatterns.add(attributes.get(PATTERN)); + + case "includedCollection" -> + includedCollections.add(StandardCollection.valueOf(attributes.get(NAME))); + + case "includedPath" -> + includedPaths.add(new DocumentNameMatcher(attributes.get(NAME), + (Predicate) x -> { + throw new NotImplementedException("Deserialized ExclusionProcessor can not evaluate paths"); + })); + + case "fileProcessor" -> + fileProcessors.add(StandardCollection.valueOf(attributes.get(NAME))); + + default -> + DefaultLog.getInstance().error(String.format("Unknown child node '%s'", child.getNodeName())); + } + } + } + } } diff --git a/apache-rat-core/src/main/java/org/apache/rat/config/parameters/Description.java b/apache-rat-core/src/main/java/org/apache/rat/config/parameters/Description.java index 6aa20bd38..a4e579dab 100644 --- a/apache-rat-core/src/main/java/org/apache/rat/config/parameters/Description.java +++ b/apache-rat-core/src/main/java/org/apache/rat/config/parameters/Description.java @@ -37,7 +37,7 @@ /** * A description of a component. */ -public class Description { +public final class Description { /** The type of component this describes */ private final ComponentType type; /** diff --git a/apache-rat-core/src/main/java/org/apache/rat/configuration/XMLConfigurationReader.java b/apache-rat-core/src/main/java/org/apache/rat/configuration/XMLConfigurationReader.java index ac11bdd5a..deb63c17e 100644 --- a/apache-rat-core/src/main/java/org/apache/rat/configuration/XMLConfigurationReader.java +++ b/apache-rat-core/src/main/java/org/apache/rat/configuration/XMLConfigurationReader.java @@ -61,6 +61,8 @@ import org.xml.sax.InputSource; import org.xml.sax.SAXException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + /** * A class that reads the XML configuration file format. */ @@ -173,7 +175,7 @@ public void read(final URI... uris) { * @param list the NodeList to process * @param consumer the consumer to apply to each node in the list. */ - private void nodeListConsumer(final NodeList list, final Consumer consumer) { + public static void nodeListConsumer(final NodeList list, final Consumer consumer) { for (int i = 0; i < list.getLength(); i++) { consumer.accept(list.item(i)); } @@ -199,7 +201,7 @@ public void add(final Document newDoc) { * @param node The node to process * @return the map of attributes on the node. */ - private Map attributes(final Node node) { + public static Map attributes(final Node node) { NamedNodeMap nnm = node.getAttributes(); Map result = new HashMap<>(); for (int i = 0; i < nnm.getLength(); i++) { @@ -606,6 +608,7 @@ public void readMatcherBuilders() { nodeListConsumer(document.getElementsByTagName(XMLConfig.MATCHER), this::parseMatcherBuilder); } + @SuppressFBWarnings("URLCONNECTION_SSRF_FD") @Override public void addMatchers(final URI uri) { read(uri); diff --git a/apache-rat-core/src/main/java/org/apache/rat/configuration/builders/ChildContainerBuilder.java b/apache-rat-core/src/main/java/org/apache/rat/configuration/builders/ChildContainerBuilder.java index d40a657d2..172314fbe 100644 --- a/apache-rat-core/src/main/java/org/apache/rat/configuration/builders/ChildContainerBuilder.java +++ b/apache-rat-core/src/main/java/org/apache/rat/configuration/builders/ChildContainerBuilder.java @@ -59,7 +59,7 @@ protected ChildContainerBuilder() { */ public AbstractBuilder setResource(final String resourceName) { // this method is called by reflection - URL url = this.getClass().getResource(resourceName); + URL url = AbstractBuilder.class.getResource(resourceName); if (url == null) { throw new ConfigurationException("Unable to read matching text file: " + resourceName); } diff --git a/apache-rat-core/src/main/java/org/apache/rat/document/ArchiveEntryDocument.java b/apache-rat-core/src/main/java/org/apache/rat/document/ArchiveEntryDocument.java deleted file mode 100644 index dabc848bc..000000000 --- a/apache-rat-core/src/main/java/org/apache/rat/document/ArchiveEntryDocument.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one * - * or more contributor license agreements. See the NOTICE file * - * distributed with this work for additional information * - * regarding copyright ownership. The ASF licenses this file * - * to you under the Apache License, Version 2.0 (the * - * "License"); you may not use this file except in compliance * - * with the License. You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, * - * software distributed under the License is distributed on an * - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * - * KIND, either express or implied. See the License for the * - * specific language governing permissions and limitations * - * under the License. * - */ - -package org.apache.rat.document; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.util.Collections; -import java.util.SortedSet; - -import org.apache.rat.api.Document; - -/** - * A Document that wraps an Archive entry. - */ -public class ArchiveEntryDocument extends Document { - - /** The contents of the entry */ - private final byte[] contents; - - /** - * Creates an Archive entry. - * @param entryName the name of this entry from outside the archive. - * @param contents the contents of the entry. - * @param nameMatcher the name matcher to filter contents with. - */ - public ArchiveEntryDocument(final ArchiveEntryName entryName, final byte[] contents, final DocumentNameMatcher nameMatcher) { - super(entryName, nameMatcher); - this.contents = contents; - } - - @Override - public InputStream inputStream() { - return new ByteArrayInputStream(contents); - } - - @Override - public boolean isDirectory() { - return false; - } - - @Override - public SortedSet listChildren() { - return Collections.emptySortedSet(); - } -} diff --git a/apache-rat-core/src/main/java/org/apache/rat/report/claim/ClaimStatistic.java b/apache-rat-core/src/main/java/org/apache/rat/report/claim/ClaimStatistic.java index 62572916c..8b0687df6 100644 --- a/apache-rat-core/src/main/java/org/apache/rat/report/claim/ClaimStatistic.java +++ b/apache-rat-core/src/main/java/org/apache/rat/report/claim/ClaimStatistic.java @@ -19,14 +19,24 @@ package org.apache.rat.report.claim; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import javax.xml.parsers.DocumentBuilder; + +import org.apache.commons.io.function.IOSupplier; import org.apache.commons.lang3.StringUtils; import org.apache.rat.api.Document; +import org.apache.rat.configuration.XMLConfigurationReader; +import org.apache.rat.report.xml.writer.XmlWriter; +import org.apache.rat.utils.StandardXmlFactory; +import org.xml.sax.SAXException; /** * This class provides a numerical overview about @@ -100,7 +110,7 @@ public int getDefaultMinValue() { * @return displayName of the counter, capitalized and without underscores. */ public String displayName() { - return StringUtils.capitalize(name().replaceAll("_", " ").toLowerCase(Locale.ROOT)); + return StringUtils.capitalize(name().replace("_", " ").toLowerCase(Locale.ROOT)); } } @@ -113,6 +123,9 @@ public String displayName() { /** Map of counter type to value */ private final ConcurrentHashMap counterMap = new ConcurrentHashMap<>(); + public Serde serde() { + return new Serde(); + } /** * Converts null counter to 0. * @@ -141,6 +154,15 @@ public void incCounter(final Counter counter, final int value) { counterMap.compute(counter, (k, v) -> v == null ? new IntCounter().increment(value) : v.increment(value)); } + /** + * Increments the counts for the counter. + * @param counter the counter to increment. + * @param value the value to increment the counter by. + */ + public void setCounter(final Counter counter, final int value) { + counterMap.put(counter, new IntCounter().increment(value)); + } + /** * Gets the counts for the Document.Type. * @param documentType the Document.Type to get the counter for. @@ -168,24 +190,12 @@ public List getDocumentTypes() { public void incCounter(final Document.Type documentType, final int value) { documentTypeMap.compute(documentType, (k, v) -> updateCounter(Counter.DOCUMENT_TYPES, v, value)); switch (documentType) { - case STANDARD: - incCounter(Counter.STANDARDS, value); - break; - case ARCHIVE: - incCounter(Counter.ARCHIVES, value); - break; - case BINARY: - incCounter(Counter.BINARIES, value); - break; - case NOTICE: - incCounter(Counter.NOTICES, value); - break; - case UNKNOWN: - incCounter(Counter.UNKNOWN, value); - break; - case IGNORED: - incCounter(Counter.IGNORED, value); - break; + case STANDARD -> incCounter(Counter.STANDARDS, value); + case ARCHIVE -> incCounter(Counter.ARCHIVES, value); + case BINARY -> incCounter(Counter.BINARIES, value); + case NOTICE -> incCounter(Counter.NOTICES, value); + case UNKNOWN -> incCounter(Counter.UNKNOWN, value); + case IGNORED -> incCounter(Counter.IGNORED, value); } } @@ -288,5 +298,105 @@ public IntCounter increment(final int count) { public int value() { return value; } + + @Override + public String toString() { + return String.valueOf(value); + } + } + + /** + * Serialze and deserialze the claim Statistic. + */ + public class Serde { + /** The count attribute string */ + private static final String COUNT = "count"; + /** the name attribute string */ + private static final String NAME = "name"; + + /** + * Serializes the claim statistic into an appendable. + * @param appendable the appendable to write to + * @throws IOException on error. + */ + public void serialize(final Appendable appendable) throws IOException { + try (XmlWriter writer = new XmlWriter(appendable)) { + writer.startDocument().startElement("ClaimStatistic") + .startElement("licenseNameMap"); + for (Map.Entry entry : licenseNameMap.entrySet()) { + if (entry.getValue().value > 0) { + writer.startElement("licenseName") + .attribute(COUNT, entry.getValue().toString()) + .attribute(NAME, entry.getKey()).closeElement(); + } + } + writer.closeElement() + .startElement("licenseFamilyCategoryMap"); + for (Map.Entry entry : licenseFamilyCategoryMap.entrySet()) { + if (entry.getValue().value > 0) { + writer.startElement("familyCategory") + .attribute(COUNT, entry.getValue().toString()) + .attribute(NAME, entry.getKey()).closeElement(); + } + } + writer.closeElement() + .startElement("documentTypeMap"); + for (Map.Entry entry : documentTypeMap.entrySet()) { + if (entry.getValue().value > 0) { + writer.startElement("documentType") + .attribute(COUNT, entry.getValue().toString()) + .attribute(NAME, entry.getKey().name()).closeElement(); + } + } + writer.closeElement() + .startElement("counterMap"); + for (Map.Entry entry : counterMap.entrySet()) { + if (entry.getValue().value > 0) { + writer.startElement("counter") + .attribute(COUNT, entry.getValue().toString()) + .attribute(NAME, entry.getKey().name()).closeElement(); + } + } + writer.closeElement(); + } + } + + /** + * Deserializes a ClaimStatistic from an input stream. + * @param inputStreamSupplier the supplier of the input stream to deserialize from. + * @throws IOException on error. + */ + public void deserialize(final IOSupplier inputStreamSupplier) throws IOException { + DocumentBuilder builder = StandardXmlFactory.documentBuilder(); + org.w3c.dom.Document document; + try (InputStream stream = inputStreamSupplier.get()) { + document = builder.parse(stream); + + } catch (SAXException e) { + throw new IOException("Unable to read input", e); + } + + XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("licenseName"), node -> { + Map attributes = XMLConfigurationReader.attributes(node); + incLicenseNameCount(attributes.get(NAME), Integer.parseInt(attributes.get(COUNT))); + }); + + XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("familyCategory"), node -> { + Map attributes = XMLConfigurationReader.attributes(node); + incLicenseCategoryCount(attributes.get(NAME), Integer.parseInt(attributes.get(COUNT))); + }); + + XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("documentType"), node -> { + Map attributes = XMLConfigurationReader.attributes(node); + Document.Type type = Document.Type.valueOf(attributes.get(NAME)); + incCounter(type, Integer.parseInt(attributes.get(COUNT))); + }); + + XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("counter"), node -> { + Map attributes = XMLConfigurationReader.attributes(node); + Counter type = Counter.valueOf(attributes.get(NAME)); + setCounter(type, Integer.parseInt(attributes.get(COUNT))); + }); + } } } diff --git a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/FileUtils.java b/apache-rat-core/src/main/java/org/apache/rat/utils/FileUtils.java similarity index 79% rename from apache-rat-core/src/test/java/org/apache/rat/testhelpers/FileUtils.java rename to apache-rat-core/src/main/java/org/apache/rat/utils/FileUtils.java index 5681e7972..453f7ec60 100644 --- a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/FileUtils.java +++ b/apache-rat-core/src/main/java/org/apache/rat/utils/FileUtils.java @@ -16,24 +16,27 @@ * specific language governing permissions and limitations * * under the License. * */ -package org.apache.rat.testhelpers; +package org.apache.rat.utils; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; +import java.nio.file.Files; import java.util.Arrays; import java.util.Collections; -import static org.assertj.core.api.Fail.fail; -public class FileUtils { +public final class FileUtils { + private FileUtils() { + // do not instantiate + } /** * Creates a directory if it does not exist. * @param dir the directory to make. */ - public static void mkDir(File dir) { + public static void mkDir(final File dir) { boolean ignored = dir.mkdirs(); } @@ -41,7 +44,7 @@ public static void mkDir(File dir) { * Deletes a file if it exists. * @param file the file to delete. */ - public static void delete(File file) { + public static void delete(final File file) throws IOException { if (file.exists()) { if (file.isDirectory()) { try { @@ -50,7 +53,7 @@ public static void delete(File file) { // } } else { - boolean ignored = file.delete(); + Files.delete(file.toPath()); } } } @@ -62,15 +65,16 @@ public static void delete(File file) { * @param lines the lines to write into the file. * @return the new File. */ - static public File writeFile(File dir, final String name, final Iterable lines) { + public static File writeFile(final File dir, final String name, final Iterable lines) { if (dir == null) { - fail("base directory not specified"); + throw new IllegalArgumentException("base directory not specified"); } + mkDir(dir); File file = new File(dir, name); try (PrintWriter writer = new PrintWriter(new FileWriter(file))) { lines.forEach(writer::println); } catch (IOException e) { - fail(e.getMessage()); + throw new RuntimeException(e.getMessage(), e); } return file; } @@ -82,7 +86,7 @@ static public File writeFile(File dir, final String name, final Iterable * @param lines the lines to write into the file. * @return the new File. */ - static public File writeFile(File dir, final String name, final String... lines) { + public static File writeFile(final File dir, final String name, final String... lines) { return writeFile(dir, name, Arrays.asList(lines)); } @@ -92,7 +96,7 @@ static public File writeFile(File dir, final String name, final String... lines) * @param name the name of the file. * @return the new file. */ - public static File writeFile(File dir, String name) { + public static File writeFile(final File dir, final String name) { return writeFile(dir, name, Collections.singletonList(name)); } } diff --git a/apache-rat-core/src/main/java/org/apache/rat/utils/StandardXmlFactory.java b/apache-rat-core/src/main/java/org/apache/rat/utils/StandardXmlFactory.java index 3ec9849a7..c7e39cc34 100644 --- a/apache-rat-core/src/main/java/org/apache/rat/utils/StandardXmlFactory.java +++ b/apache-rat-core/src/main/java/org/apache/rat/utils/StandardXmlFactory.java @@ -18,7 +18,13 @@ */ package org.apache.rat.utils; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; import java.io.InputStream; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.Properties; import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; @@ -27,9 +33,17 @@ import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; +import org.apache.commons.lang3.StringUtils; +import org.w3c.dom.Document; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + /** * Factory to create standard XML objects. The intention of this class is to resolve in a consistent manner the * XXE errors and similar XML IO errors. @@ -53,16 +67,29 @@ private StandardXmlFactory() { * @throws TransformerConfigurationException on error. */ public static Transformer createTransformer() throws TransformerConfigurationException { - return createTransformer(null); + return createTransformer(null, new Properties()); } /** * Create a transformer with the specified stylesheet. - * @param styleIn the stylesheet to use. + * @param styleIn the stylesheet input stream. * @return the transformer. * @throws TransformerConfigurationException on error. */ + @SuppressFBWarnings("MALICIOUS_XSLT") public static Transformer createTransformer(final InputStream styleIn) throws TransformerConfigurationException { + return createTransformer(styleIn, new Properties()); + } + + /** + * Create a transformer with the specified stylesheet and additional properties. + * By default, the output omits the XML declaration, uses XML output, indents rsult with 4 spaces. + * @param styleIn the stylesheet to use. + * @param transformerProperties Additional output transformer properties. + * @return the transformer. + * @throws TransformerConfigurationException on error. + */ + public static Transformer createTransformer(final InputStream styleIn, final Properties transformerProperties) throws TransformerConfigurationException { TransformerFactory factory = TransformerFactory.newInstance(); // NOSONAR factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); @@ -73,6 +100,9 @@ public static Transformer createTransformer(final InputStream styleIn) throws Tr transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); + if (transformerProperties != null) { + transformer.setOutputProperties(transformerProperties); + } return transformer; } @@ -94,4 +124,29 @@ public static DocumentBuilder documentBuilder() { throw new IllegalStateException("No XML parser defined", e); } } + + /** + * Write an XML document to a file. + * @param document the document to write + * @param file the file to write to. + */ + public static void writeDocument(final Document document, final File file) throws IOException, TransformerException { + DOMSource source = new DOMSource(document); + FileWriter writer = new FileWriter(file, StandardCharsets.UTF_8); + StreamResult result = new StreamResult(writer); + createTransformer().transform(source, result); + } + + /** + * Write an XML document to a file. + * @param document the document to write. + */ + public static String serializeDocument(final Document document) throws TransformerException { + DOMSource source = new DOMSource(document); + StringWriter writer = new StringWriter(); + StreamResult sink = new StreamResult(writer); + createTransformer().transform(source, sink); + String result = writer.toString(); + return StringUtils.defaultIfBlank(result, ""); + } } diff --git a/apache-rat-core/src/main/java/org/apache/rat/walker/ArchiveWalker.java b/apache-rat-core/src/main/java/org/apache/rat/walker/ArchiveWalker.java index a50aa5ef8..4ad410449 100644 --- a/apache-rat-core/src/main/java/org/apache/rat/walker/ArchiveWalker.java +++ b/apache-rat-core/src/main/java/org/apache/rat/walker/ArchiveWalker.java @@ -20,12 +20,15 @@ package org.apache.rat.walker; import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.SortedSet; import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.ArchiveException; @@ -34,9 +37,9 @@ import org.apache.commons.io.IOUtils; import org.apache.rat.api.Document; import org.apache.rat.api.RatException; -import org.apache.rat.document.ArchiveEntryDocument; import org.apache.rat.document.ArchiveEntryName; import org.apache.rat.document.DocumentName; +import org.apache.rat.document.DocumentNameMatcher; import org.apache.rat.report.RatReport; import org.apache.rat.utils.DefaultLog; @@ -87,13 +90,29 @@ public Collection getDocuments() throws RatException { ArchiveEntry entry; while ((entry = input.getNextEntry()) != null) { if (!entry.isDirectory() && input.canReadEntryData(entry)) { - DocumentName innerName = DocumentName.builder().setName(entry.getName()) + final DocumentName innerName = DocumentName.builder().setName(entry.getName()) .setBaseName(".").build(); - if (this.getDocument().getNameMatcher().matches(innerName)) { + final DocumentNameMatcher documentNameMatcher = getDocument().getNameMatcher(); + if (documentNameMatcher.matches(innerName)) { + ArchiveEntryName entryName = new ArchiveEntryName(getDocument().getName(), entry.getName()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); IOUtils.copy(input, baos); - ArchiveEntryName entryName = new ArchiveEntryName(getDocument().getName(), entry.getName()); - result.add(new ArchiveEntryDocument(entryName, baos.toByteArray(), getDocument().getNameMatcher())); + result.add(new Document(entryName, documentNameMatcher) { + @Override + public InputStream inputStream() { + return new ByteArrayInputStream(baos.toByteArray()); + } + + @Override + public boolean isDirectory() { + return false; + } + + @Override + public SortedSet listChildren() { + return Collections.emptySortedSet(); + } + }); } } } diff --git a/apache-rat-core/src/main/resources/org/apache/rat/xhtml5.xsl b/apache-rat-core/src/main/resources/org/apache/rat/xhtml5.xsl new file mode 100644 index 000000000..b15224605 --- /dev/null +++ b/apache-rat-core/src/main/resources/org/apache/rat/xhtml5.xsl @@ -0,0 +1,354 @@ + + + + + + + + + ❗ + 🗜 + 🔠 + 🚫 + ℹ + ✅ + ❓ + 📂 + + + + + + + + + + + + + + + + Rat Report + + + + + + + + + + + + + Detail + + + Documents with unapproved licenses will start with a + The first character on the next line identifies the document type. + + + + Symbols used in this RAT report + + + Symbol + Type + + + + Archive file + + + + Binary file + + + + Ignored file + + + + Notice file + + + + Standard file + + + + Unknown file + + + + Directory + + + + + + Resources discovered in this RAT report + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ID: + + UUID + + + + Family: + + Unknown + + + + + + + + + + + + + Summary + + + Generated at by + + + + + + Table 1: A summary of statistics from this RAT report. + + + Name: + Count: + Description: + + + + + + + + + Unknown + + + + + + + + + + License Statistics + + Categories + + + + Table 2: License categories found in this RAT report. + + + Name: + Count: + + + + + + + + + + Unknown + + + + + + + + + + Licenses + + + + Table 3: Licenses found in this RAT report. + + + Name: + Count: + + + + + + + + + + Unknown + + + + + + + + + + + Document types + + + + Table 4: Document types found in this RAT report. + + + Name: + Count: + + + + + + + + + + + + + + + Files with unapproved licenses + + + + + + + + + + Archives + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apache-rat-core/src/test/java/org/apache/rat/OptionCollectionParserTest.java b/apache-rat-core/src/test/java/org/apache/rat/OptionCollectionParserTest.java index f18ee2ab9..2b2411497 100644 --- a/apache-rat-core/src/test/java/org/apache/rat/OptionCollectionParserTest.java +++ b/apache-rat-core/src/test/java/org/apache/rat/OptionCollectionParserTest.java @@ -19,7 +19,7 @@ package org.apache.rat; import org.apache.commons.cli.Option; -import org.apache.commons.cli.ParseException; +import org.apache.rat.api.RatException; import org.apache.rat.commandline.ArgumentContext; import org.apache.rat.ui.UIOption; import org.apache.rat.ui.UIOptionCollection; @@ -28,7 +28,6 @@ import org.junit.jupiter.api.io.CleanupMode; import org.junit.jupiter.api.io.TempDir; -import java.io.IOException; import java.nio.file.Path; import static org.assertj.core.api.Assertions.assertThat; @@ -42,7 +41,7 @@ class OptionCollectionParserTest { private final OptionCollectionParser underTest = new OptionCollectionParser(optionCollection); @Test - void parseCommands() throws IOException, ParseException { + void parseCommands() throws RatException { String[] args = {"arg1", "arg2"}; ArgumentContext ctxt = underTest.parseCommands(testPath.toFile(), args); assertThat(ctxt.getCommandLine().getArgList()).containsExactly(args); diff --git a/apache-rat-core/src/test/java/org/apache/rat/OptionCollectionTest.java b/apache-rat-core/src/test/java/org/apache/rat/OptionCollectionTest.java index 070484f27..9b2592acf 100644 --- a/apache-rat-core/src/test/java/org/apache/rat/OptionCollectionTest.java +++ b/apache-rat-core/src/test/java/org/apache/rat/OptionCollectionTest.java @@ -30,8 +30,7 @@ import java.util.Locale; import java.util.Map; import java.util.TreeMap; -import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.DefaultParser; + import org.apache.commons.cli.Option; import org.apache.commons.cli.ParseException; import org.apache.commons.lang3.tuple.Pair; @@ -44,6 +43,7 @@ import org.apache.rat.testhelpers.TestingLog; import org.apache.rat.utils.CasedString; import org.apache.rat.utils.DefaultLog; +import org.apache.rat.utils.FileUtils; import org.apache.rat.utils.Log; import org.apache.rat.walker.ArchiveWalker; import org.apache.rat.walker.DirectoryWalker; @@ -181,6 +181,7 @@ public static Map processTestFunctionAnnotations(Object test public void testDeprecatedUseLogged() throws IOException { TestingLog log = new TestingLog(); try { + FileUtils.mkDir(testPath.resolve("target").toFile()); DefaultLog.setInstance(log); String[] args = {"--dir", "target", "-a"}; ReportConfiguration config = OptionCollection.parseCommands(testPath.toFile(), args, o -> fail("Help printed"), true); @@ -220,8 +221,7 @@ public void testShortenedOptions() throws IOException { @Test public void testDefaultConfiguration() throws ParseException { String[] empty = {}; - CommandLine cl = new DefaultParser().parse(OptionCollection.buildOptions(), empty); - ArgumentContext context = new ArgumentContext(new File("."), cl); + ArgumentContext context = new ArgumentContext(new File("."), OptionCollection.buildOptions(), empty); ReportConfiguration config = OptionCollection.createConfiguration(context); ReportConfigurationTest.validateDefault(config); } diff --git a/apache-rat-core/src/test/java/org/apache/rat/OutputTest.java b/apache-rat-core/src/test/java/org/apache/rat/OutputTest.java new file mode 100644 index 000000000..99c53e072 --- /dev/null +++ b/apache-rat-core/src/test/java/org/apache/rat/OutputTest.java @@ -0,0 +1,284 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * https://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + */ +package org.apache.rat; + +import org.apache.commons.io.function.IOSupplier; +import org.apache.rat.api.RatException; +import org.apache.rat.commandline.StyleSheets; +import org.apache.rat.config.AddLicenseHeaders; +import org.apache.rat.config.exclusion.StandardCollection; +import org.apache.rat.config.results.ClaimValidator; +import org.apache.rat.document.DocumentName; +import org.apache.rat.document.DocumentNameMatcher; +import org.apache.rat.document.FileDocument; +import org.apache.rat.license.LicenseSetFactory; +import org.apache.rat.report.claim.ClaimStatistic; +import org.apache.rat.report.claim.ClaimStatisticTest; +import org.apache.rat.test.utils.Resources; +import org.apache.rat.utils.FileUtils; +import org.apache.rat.utils.StandardXmlFactory; +import org.apache.rat.utils.StandardXmlFactoryTests; +import org.apache.rat.walker.DirectoryWalker; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; +import org.xmlunit.assertj3.XmlAssert; + +import javax.xml.transform.TransformerException; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class OutputTest { + + static Path tempPath; + + @BeforeAll + static void setup() throws IOException { + tempPath = Files.createTempDirectory("outputTest").toAbsolutePath(); + } + + @AfterAll + static void teardown() { + FileUtils.delete(tempPath.toFile()); + } + + @Test + void documentReadingTest() throws IOException, SAXException, TransformerException { + Path testPath = tempPath.resolve("documentReading"); + File testFile = testPath.toFile(); + FileUtils.mkDir(testFile); + DocumentName workingDirectory = DocumentName.builder(testPath.toFile()).setBaseName(testFile).build(); + Document document; + DocumentName documentFile = workingDirectory.resolve("document.xml"); + try (InputStream inputStream = OutputTest.class.getClassLoader().getResourceAsStream("XmlOutputExamples/elements.xml")) { + document = StandardXmlFactory.documentBuilder().parse(inputStream); + StandardXmlFactory.writeDocument(document, documentFile.asFile()); + } + + Reporter.Output.Builder builder = Reporter.Output.builder().document(documentFile.getName(), workingDirectory); + + Reporter.Output output = builder.build(); + XmlAssert.assertThat(output.getDocument()).and(document) + .ignoreWhitespace() + .areIdentical(); + } + + @Test + void statisticReadingTest() throws IOException { + Path testPath = tempPath.resolve("statisticReading"); + File testFile = testPath.toFile(); + FileUtils.mkDir(testFile); + DocumentName workingDirectory = DocumentName.builder(testPath.toFile()).setBaseName(testFile).build(); + DocumentName documentFile = workingDirectory.resolve("statistic.xml"); + + ClaimStatistic underTest = new ClaimStatistic(); + underTest.incLicenseCategoryCount("familyCategory", 1); + underTest.incCounter(ClaimStatistic.Counter.APPROVED, 2); + underTest.incCounter(org.apache.rat.api.Document.Type.IGNORED, 3); + underTest.incLicenseNameCount("licenseName", 4); + + ClaimStatistic.Serde serde = underTest.serde(); + StringWriter stringWriter = new StringWriter(); + serde.serialize(stringWriter); + try (FileOutputStream fos = new FileOutputStream(documentFile.asFile())) { + fos.write(stringWriter.toString().getBytes(StandardCharsets.UTF_8)); + } + + Reporter.Output.Builder builder = Reporter.Output.builder().statistic(documentFile.getName(), workingDirectory); + + Reporter.Output output = builder.build(); + ClaimStatisticTest.assertSame(output.getStatistic(), underTest); + } + + @Test + void ReadingBadFileTest() { + Path testPath = tempPath.resolve("readingBadFileTest"); + File testFile = testPath.toFile(); + FileUtils.mkDir(testFile); + DocumentName workingDirectory = DocumentName.builder(testPath.toFile()).setBaseName(testFile).build(); + DocumentName documentFile = workingDirectory.resolve("missing.file"); + String name = documentFile.getName(); + Reporter.Output.Builder builder = Reporter.Output.builder(); + assertThatThrownBy(() -> builder.statistic(name, workingDirectory)) + .as("statistic read") + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("Unable to read file: " + testPath.resolve("missing.file")); + + assertThatThrownBy(() -> builder.configuration(name, workingDirectory)) + .as("configuration read") + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("Unable to read file: " + testPath.resolve("missing.file")); + + assertThatThrownBy(() -> builder.document(name, workingDirectory)) + .as("document read") + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("Unable to read file: " + testPath.resolve("missing.file")); + } + + @Test + void configurationReadingTest() throws IOException { + Path testPath = tempPath.resolve("configurationReading"); + File testFile = testPath.toFile(); + FileUtils.mkDir(testFile); + DocumentName workingDirectory = DocumentName.builder(testPath.toFile()).setBaseName(testFile).build(); + DocumentName documentFile = workingDirectory.resolve("configuration.xml"); + + ReportConfiguration underTest = new ReportConfiguration(); + underTest.setAddLicenseHeaders(AddLicenseHeaders.FORCED); + underTest.listFamilies(LicenseSetFactory.LicenseFilter.APPROVED); + underTest.listLicenses(LicenseSetFactory.LicenseFilter.ALL); + underTest.setDryRun(true); + underTest.setArchiveProcessing(ReportConfiguration.Processing.NOTIFICATION); + underTest.setStandardProcessing(ReportConfiguration.Processing.ABSENCE); + underTest.setStyleSheet(StyleSheets.MISSING_HEADERS.getStyleSheet()); + underTest.setOut(new File("/some/file/somewhere")); + underTest.setCopyrightMessage("the copyright message"); + underTest.addSource(new File("/my/file")); + underTest.addSource(new ReportConfigurationTest.TestingReportable()); + underTest.addExcludedPatterns(List.of("pattern/**", "pattern2/**")); + underTest.addExcludedCollection(StandardCollection.BAZAAR); + underTest.addExcludedCollection(StandardCollection.MISC); + underTest.addExcludedFileProcessor(StandardCollection.HIDDEN_FILE); + underTest.addExcludedMatcher(DocumentNameMatcher.MATCHES_ALL); + underTest.addIncludedPatterns(List.of("**/pattern3", "**/pattern4")); + underTest.addIncludedCollection(StandardCollection.ARCH); + underTest.addIncludedCollection(StandardCollection.BITKEEPER); + underTest.addIncludedMatcher(DocumentNameMatcher.MATCHES_NONE); + + ClaimValidator claimValidator = underTest.getClaimValidator(); + claimValidator.setMax(ClaimStatistic.Counter.APPROVED, 5); + claimValidator.setMin(ClaimStatistic.Counter.APPROVED, 3); + claimValidator.setMax(ClaimStatistic.Counter.ARCHIVES, 10); + claimValidator.setMin(ClaimStatistic.Counter.BINARIES, 4); + + StringWriter stringWriter = new StringWriter(); + underTest.serde().serialize(stringWriter); + + try (FileOutputStream fos = new FileOutputStream(documentFile.asFile())) { + fos.write(stringWriter.toString().getBytes(StandardCharsets.UTF_8)); + } + + Reporter.Output.Builder builder = Reporter.Output.builder().configuration(documentFile.getName(), workingDirectory); + + Reporter.Output output = builder.build(); + ReportConfigurationTest.assertSame(output.getConfiguration(), underTest); + } + + private ReportConfiguration initializeConfiguration() throws URISyntaxException { + Defaults defaults = Defaults.builder().build(); + final File elementsFile = Resources.getExampleResource("exampleData"); + final ReportConfiguration configuration = new ReportConfiguration(); + configuration.setFrom(defaults); + DocumentName documentName = DocumentName.builder(elementsFile).build(); + configuration.addSource(new DirectoryWalker(new FileDocument(documentName, elementsFile, + configuration.getDocumentExcluder(documentName)))); + return configuration; + } + + @Test + void listLicensesReportTest() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ReportConfiguration configuration = initializeConfiguration(); + configuration.setOut(new ReportConfiguration.IODescriptor<>("listLicensesReportTest", () -> out)); + configuration.setStyleSheet(StyleSheets.UNAPPROVED_LICENSES.getStyleSheet()); + Reporter.Output output = Reporter.Output.builder() + .statistic(new ClaimStatistic()) + .document(null) + .configuration(configuration) + .build(); + output.listLicenses(LicenseSetFactory.LicenseFilter.NONE); + + out.flush(); + String document = out.toString(); + assertThat(document).contains("Licenses (NONE):"); + StringWriter writer = new StringWriter(); + try (PrintWriter printWriter = new PrintWriter(writer)) { + output.listLicenses(printWriter, LicenseSetFactory.LicenseFilter.ALL); + } + assertThat(writer.toString()).contains("Licenses (ALL):"); + } + + @Test + void formatTest() throws IOException, SAXException { + Reporter.Output output = Reporter.Output.builder() + .statistic(new ClaimStatistic()) + .document(StandardXmlFactoryTests.simpleDocument()) + .configuration(new ReportConfiguration()) + .build(); + + IOSupplier badStyleSheet = () -> new InputStream() { + @Override + public int read() throws IOException { + throw new IOException("Expected exception."); + } + }; + assertThatThrownBy(() -> output.format(badStyleSheet, ReportConfiguration.SYSTEM_OUT.ioSupplier())) + .as("badStyleSheet") + .isInstanceOf(RatException.class) + .hasMessageContaining("Expected exception") + .hasCauseInstanceOf(TransformerException.class); + + IOSupplier missingStyleSheet = () -> {throw new IOException("Expected exception.");}; + + + assertThatThrownBy(() -> output.format(missingStyleSheet, ReportConfiguration.SYSTEM_OUT.ioSupplier())) + .as("missingStyleSheet") + .isInstanceOf(RatException.class) + .hasMessageContaining("Expected exception") + .hasCauseInstanceOf(IOException.class); + + IOSupplier badOutput = () -> new OutputStream() { + @Override + public void write(int b) throws IOException { + throw new IOException("Expected exception."); + } + }; + assertThatThrownBy(() -> output.format(StyleSheets.XML.getStyleSheet().ioSupplier(), badOutput)) + .as("badOutput") + .isInstanceOf(RatException.class) + .hasMessageContaining("Expected exception") + .hasCauseInstanceOf(TransformerException.class); + + IOSupplier missingOutput = () -> {throw new IOException("Expected exception.");}; + assertThatThrownBy(() -> output.format(StyleSheets.XML.getStyleSheet().ioSupplier(), missingOutput)) + .as("missingOutput") + .isInstanceOf(RatException.class) + .hasMessageContaining("Expected exception") + .hasCauseInstanceOf(IOException.class); + + } + + +} diff --git a/apache-rat-core/src/test/java/org/apache/rat/ReportConfigurationTest.java b/apache-rat-core/src/test/java/org/apache/rat/ReportConfigurationTest.java index 7d6040433..a8accb3b9 100644 --- a/apache-rat-core/src/test/java/org/apache/rat/ReportConfigurationTest.java +++ b/apache-rat-core/src/test/java/org/apache/rat/ReportConfigurationTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.when; import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileFilter; @@ -32,31 +33,41 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; +import java.io.StringWriter; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.SortedSet; import java.util.function.Function; +import java.util.stream.Collectors; import org.apache.commons.io.filefilter.DirectoryFileFilter; import org.apache.commons.io.output.CloseShieldOutputStream; import org.apache.rat.analysis.IHeaderMatcher; +import org.apache.rat.api.RatException; +import org.apache.rat.commandline.StyleSheets; import org.apache.rat.config.AddLicenseHeaders; +import org.apache.rat.config.exclusion.ExclusionProcessorTest; import org.apache.rat.config.exclusion.StandardCollection; +import org.apache.rat.config.results.ClaimValidator; import org.apache.rat.configuration.XMLConfigurationReaderTest; import org.apache.rat.document.DocumentName; import org.apache.rat.document.DocumentNameMatcher; import org.apache.rat.license.ILicense; import org.apache.rat.license.ILicenseFamily; import org.apache.rat.license.LicenseSetFactory.LicenseFilter; +import org.apache.rat.report.RatReport; import org.apache.rat.report.Reportable; +import org.apache.rat.report.claim.ClaimStatistic; import org.apache.rat.testhelpers.TestingLog; import org.apache.rat.testhelpers.TestingLicense; import org.apache.rat.testhelpers.TestingMatcher; import org.apache.rat.utils.DefaultLog; +import org.apache.rat.utils.Log; import org.apache.rat.utils.Log.Level; import org.apache.rat.utils.ReportingSet.Options; import org.junit.jupiter.api.AfterEach; @@ -463,7 +474,7 @@ void outputTest() throws IOException { assertThat(underTest.getWriter()).isNotNull(); ByteArrayOutputStream stream = new ByteArrayOutputStream(); - underTest.setOut(new ReportConfiguration.IODescriptor("outputTest", () -> stream)); + underTest.setOut(new ReportConfiguration.IODescriptor<>("outputTest", () -> stream)); assertThat(underTest.getOutput().get()).isEqualTo(stream); PrintWriter writer = underTest.getWriter().get(); assertThat(writer).isNotNull(); @@ -490,7 +501,7 @@ void stylesheetTest() throws IOException, URISyntaxException { assertThat(underTest.getStyleSheetDescriptor()).isNull(); assertThat(underTest.getStyleSheet()).isNull(); InputStream stream = mock(InputStream.class); - underTest.setStyleSheet(new ReportConfiguration.IODescriptor("stylesheetTest", () -> stream)); + underTest.setStyleSheet(new ReportConfiguration.IODescriptor<>("stylesheetTest", () -> stream)); assertThat(underTest.getStyleSheetDescriptor().ioSupplier().get()).isEqualTo(stream); assertThat(underTest.getStyleSheet().get()).isEqualTo(stream); @@ -527,31 +538,37 @@ void testFlags() { @Test void testValidate() { - final StringBuilder sb = new StringBuilder(); + TestingLog testLog = new TestingLog(); + Log oldLog = DefaultLog.getInstance(); + try { + DefaultLog.setInstance(testLog); + String msg = "At least one source must be specified"; - assertThatThrownBy(() -> underTest.validate(sb::append)).isExactlyInstanceOf(ConfigurationException.class) + assertThatThrownBy(underTest::validate).isExactlyInstanceOf(ConfigurationException.class) .hasMessageContaining(msg); - assertThat(sb.toString()).isEqualTo(msg); + testLog.assertContains(msg); + testLog.clear(); + - sb.setLength(0); - msg = "You must specify at least one license"; + msg = "At least one license must be defined"; underTest.addSource(mock(Reportable.class)); - assertThatThrownBy(() -> underTest.validate(sb::append)).isExactlyInstanceOf(ConfigurationException.class) + assertThatThrownBy(underTest::validate).isExactlyInstanceOf(ConfigurationException.class) .hasMessageContaining(msg); - assertThat(sb.toString()).isEqualTo(msg); + testLog.assertContains(msg); - sb.setLength(0); underTest.addLicense(testingLicense("valid", "Validation testing license")); - underTest.validate(sb::append); - assertThat(sb.length()).isEqualTo(0); + underTest.validate(); + } finally { + DefaultLog.setInstance(oldLog); + } } @Test void testSetOut() throws IOException { ReportConfiguration config = new ReportConfiguration(); try (OutputStreamInterceptor osi = new OutputStreamInterceptor()) { - config.setOut(new ReportConfiguration.IODescriptor("testSetOut",() -> osi)); + config.setOut(new ReportConfiguration.IODescriptor<>("testSetOut", () -> osi)); assertThat(osi.closeCount).isEqualTo(0); try (OutputStream os = config.getOutput().get()) { assertThat(os).isNotNull(); @@ -574,16 +591,18 @@ void logFamilyCollisionTest() { // verify default collision logs WARNING underTest.addFamily(ILicenseFamily.builder().setLicenseFamilyCategory("CAT").setLicenseFamilyName("name2")); - assertThat(log.getCaptured().contains("WARN")).as("default value not WARN").isTrue(); - assertThat(log.getCaptured().contains("CAT")).as("'CAT' not found").isTrue(); + assertThat(log.getCaptured()).contains("WARN").contains("CAT"); // verify level setting works. for (Level l : Level.values()) { log.clear(); underTest.logFamilyCollisions(l); underTest.addFamily(ILicenseFamily.builder().setLicenseFamilyCategory("CAT").setLicenseFamilyName("name2")); - assertThat(log.getCaptured().contains("CAT")).as("'CAT' not found").isTrue(); - assertThat(log.getCaptured().contains(l.name())).as("logging not set to "+l).isTrue(); + if (DefaultLog.getInstance().isEnabled(l)) { + assertThat(log.getCaptured()).contains("CAT").contains(l.name()); + } else { + assertThat(log.getCaptured()).doesNotContain("CAT").doesNotContain(l.name()); + } } } @@ -698,51 +717,52 @@ public Appendable append(char c) throws IOException { .contains("java.io.IOException: BANG!"); } + /** * Validates that the configuration contains the default approved licenses. * @param config the configuration to test. */ - public static void validateDefaultApprovedLicenses(ReportConfiguration config) { - validateDefaultApprovedLicenses(config, 0); - } - + public static void validateDefaultApprovedLicenses(ReportConfiguration config, String... additionalIds) { + validateLicenses(config, Arrays.asList(additionalIds), LicenseFilter.APPROVED, XMLConfigurationReaderTest.APPROVED_LICENSES); + } + /** - * Validates that the configuration contains the default approved licenses. + * Validates that the configuration contains all the default licenses along with any addiitonal licenses * @param config the configuration to test. + * @param additionalLicenses Additional licence IDs that are expected. */ - public static void validateDefaultApprovedLicenses(ReportConfiguration config, int additionalIdCount) { - assertThat(config.getLicenseCategories(LicenseFilter.APPROVED)).hasSize(XMLConfigurationReaderTest.APPROVED_IDS.length + additionalIdCount); - for (String s : XMLConfigurationReaderTest.APPROVED_IDS) { - assertThat(config.getLicenseCategories(LicenseFilter.APPROVED)).contains(ILicenseFamily.makeCategory(s)); - } + public static void validateDefaultLicenses(ReportConfiguration config, String...additionalLicenses) { + validateLicenses(config, Arrays.asList(additionalLicenses), LicenseFilter.ALL, XMLConfigurationReaderTest.EXPECTED_LICENSES); } + private static void validateLicenses(ReportConfiguration config, List additionalIds, LicenseFilter filter, String[] approvedIds) { + List expected = new ArrayList<>(Arrays.asList(approvedIds)); + expected.addAll(additionalIds); + assertThat(config.getLicenses(filter).stream().map(ILicense::getId).collect(Collectors.toSet())).containsExactlyInAnyOrderElementsOf(expected); + } + + /** * Validates that the configuration contains the default license families. * @param config the configuration to test. */ public static void validateDefaultLicenseFamilies(ReportConfiguration config, String...additionalIds) { - assertThat(config.getLicenseFamilies(LicenseFilter.ALL)).hasSize(XMLConfigurationReaderTest.EXPECTED_IDS.length + additionalIds.length); - List expected = new ArrayList<>(); - expected.addAll(Arrays.asList(XMLConfigurationReaderTest.EXPECTED_IDS)); - expected.addAll(Arrays.asList(additionalIds)); - for (ILicenseFamily family : config.getLicenseFamilies(LicenseFilter.ALL)) { - assertThat(expected).contains(family.getFamilyCategory().trim()); - } + validateLicenseFamilies(config, Arrays.asList(additionalIds), LicenseFilter.ALL, XMLConfigurationReaderTest.EXPECTED_IDS); } /** - * Validates that the configuration contains the default licenses. + * Validates that the configuration contains the default license families. * @param config the configuration to test. */ - public static void validateDefaultLicenses(ReportConfiguration config, String...additionalLicenses) { - assertThat(config.getLicenses(LicenseFilter.ALL)).hasSize(XMLConfigurationReaderTest.EXPECTED_LICENSES.length + additionalLicenses.length); - List expected = new ArrayList<>(); - expected.addAll(Arrays.asList(XMLConfigurationReaderTest.EXPECTED_LICENSES)); - expected.addAll(Arrays.asList(additionalLicenses)); - for (ILicense license : config.getLicenses(LicenseFilter.ALL)) { - assertThat(expected).contains(license.getId()); + public static void validateDefaultApprovedLicenseFamilies(ReportConfiguration config, String...additionalIds) { + validateLicenseFamilies(config, Arrays.asList(additionalIds), LicenseFilter.APPROVED, XMLConfigurationReaderTest.APPROVED_IDS); } + + private static void validateLicenseFamilies(ReportConfiguration config, List additionalIds, LicenseFilter filter, String[] approvedIds) { + List expected = new ArrayList<>(Arrays.asList(approvedIds)); + expected.addAll(additionalIds); + assertThat(config.getLicenseFamilies(filter).stream().map(lf -> lf.getFamilyCategory().trim()) + .collect(Collectors.toSet())).containsExactlyInAnyOrderElementsOf(expected); } /** @@ -755,9 +775,126 @@ public static void validateDefault(ReportConfiguration config) { assertThat(config.getCopyrightMessage()).isNull(); assertThat(config.getStyleSheet()).withFailMessage("Stylesheet should not be null").isNotNull(); - validateDefaultApprovedLicenses(config); validateDefaultLicenseFamilies(config); + validateDefaultApprovedLicenseFamilies(config); validateDefaultLicenses(config); + validateDefaultApprovedLicenses(config); + } + + public static void assertSame(ReportConfiguration actual, ReportConfiguration expected) { + assertThat(actual.isAddingLicenses()).isEqualTo(expected.isAddingLicenses()); + assertThat(actual.isAddingLicensesForced()).isEqualTo(expected.isAddingLicensesForced()); + assertThat(actual.listFamilies()).isEqualTo(expected.listFamilies()); + assertThat(actual.listLicenses()).isEqualTo(expected.listLicenses()); + assertThat(actual.isDryRun()).isEqualTo(expected.isDryRun()); + assertThat(actual.getArchiveProcessing()).isEqualTo(expected.getArchiveProcessing()); + assertThat(actual.getStandardProcessing()).isEqualTo(expected.getStandardProcessing()); + assertThat(actual.getStyleSheetDescriptor().name()).isEqualTo(expected.getStyleSheetDescriptor().name()); + assertThat(actual.getOutputDescriptor().name()).isEqualTo(expected.getOutputDescriptor().name()); + + assertThat(actual.getCopyrightMessage()).isEqualTo(expected.getCopyrightMessage()); + + assertThat(actual.sources()).containsExactlyElementsOf(expected.sources()); + assertThat(actual.reportables()).containsExactlyElementsOf(expected.reportables().toList()); + ExclusionProcessorTest.assertSame(actual.getExclusionProcessor(), expected.getExclusionProcessor()); + + } + @Test + void serdeTest() throws IOException { + underTest.setAddLicenseHeaders(AddLicenseHeaders.FORCED); + underTest.listFamilies(LicenseFilter.APPROVED); + underTest.listLicenses(LicenseFilter.ALL); + underTest.setDryRun(true); + underTest.setArchiveProcessing(ReportConfiguration.Processing.NOTIFICATION); + underTest.setStandardProcessing(ReportConfiguration.Processing.ABSENCE); + underTest.setStyleSheet(StyleSheets.MISSING_HEADERS.getStyleSheet()); + underTest.setOut(new File("/some/file/somewhere")); + underTest.setCopyrightMessage("the copyright message"); + underTest.addSource(new File("/my/file")); + underTest.addSource(new TestingReportable()); + + underTest.addExcludedPatterns(List.of("pattern/**", "pattern2/**")); + underTest.addExcludedCollection(StandardCollection.BAZAAR); + underTest.addExcludedCollection(StandardCollection.MISC); + underTest.addExcludedFileProcessor(StandardCollection.HIDDEN_FILE); + underTest.addExcludedMatcher(DocumentNameMatcher.MATCHES_ALL); + underTest.addIncludedPatterns(List.of("**/pattern3", "**/pattern4")); + underTest.addIncludedCollection(StandardCollection.ARCH); + underTest.addIncludedCollection(StandardCollection.BITKEEPER); + underTest.addIncludedMatcher(DocumentNameMatcher.MATCHES_NONE); + + ClaimValidator claimValidator = underTest.getClaimValidator(); + + claimValidator.setMax(ClaimStatistic.Counter.APPROVED, 5); + claimValidator.setMin(ClaimStatistic.Counter.APPROVED, 3); + claimValidator.setMax(ClaimStatistic.Counter.ARCHIVES, 10); + claimValidator.setMin(ClaimStatistic.Counter.BINARIES, 4); + + StringWriter stringWriter = new StringWriter(); + underTest.serde().serialize(stringWriter); + + ReportConfiguration actual = new ReportConfiguration(); + actual.serde().deserialize(() -> new ByteArrayInputStream(stringWriter.toString().getBytes(StandardCharsets.UTF_8)), + DocumentName.builder(new File("/rootDir")).build()); + assertSame(actual, underTest); + } + + public static void assertSame(ReportConfiguration actual, ReportConfiguration expected) { + assertThat(actual.isAddingLicenses()).isEqualTo(expected.isAddingLicenses()); + assertThat(actual.isAddingLicensesForced()).isEqualTo(expected.isAddingLicensesForced()); + assertThat(actual.listFamilies()).isEqualTo(expected.listFamilies()); + assertThat(actual.listLicenses()).isEqualTo(expected.listLicenses()); + assertThat(actual.isDryRun()).isEqualTo(expected.isDryRun()); + assertThat(actual.getArchiveProcessing()).isEqualTo(expected.getArchiveProcessing()); + assertThat(actual.getStandardProcessing()).isEqualTo(expected.getStandardProcessing()); + assertThat(actual.getStyleSheetDescriptor().name()).isEqualTo(expected.getStyleSheetDescriptor().name()); + assertThat(actual.getOutputDescriptor().name()).isEqualTo(expected.getOutputDescriptor().name()); + + assertThat(actual.getCopyrightMessage()).isEqualTo(expected.getCopyrightMessage()); + + assertThat(actual.sources()).containsExactlyElementsOf(expected.sources()); + assertThat(actual.reportables()).containsExactlyElementsOf(expected.reportables().toList()); + ExclusionProcessorTest.assertSame(actual.getExclusionProcessor(), expected.getExclusionProcessor()); + + } + @Test + void serdeTest() throws IOException { + underTest.setAddLicenseHeaders(AddLicenseHeaders.FORCED); + underTest.listFamilies(LicenseFilter.APPROVED); + underTest.listLicenses(LicenseFilter.ALL); + underTest.setDryRun(true); + underTest.setArchiveProcessing(ReportConfiguration.Processing.NOTIFICATION); + underTest.setStandardProcessing(ReportConfiguration.Processing.ABSENCE); + underTest.setStyleSheet(StyleSheets.MISSING_HEADERS.getStyleSheet()); + underTest.setOut(new File("/some/file/somewhere")); + underTest.setCopyrightMessage("the copyright message"); + underTest.addSource(new File("/my/file")); + underTest.addSource(new TestingReportable()); + + underTest.addExcludedPatterns(List.of("pattern/**", "pattern2/**")); + underTest.addExcludedCollection(StandardCollection.BAZAAR); + underTest.addExcludedCollection(StandardCollection.MISC); + underTest.addExcludedFileProcessor(StandardCollection.HIDDEN_FILE); + underTest.addExcludedMatcher(DocumentNameMatcher.MATCHES_ALL); + underTest.addIncludedPatterns(List.of("**/pattern3", "**/pattern4")); + underTest.addIncludedCollection(StandardCollection.ARCH); + underTest.addIncludedCollection(StandardCollection.BITKEEPER); + underTest.addIncludedMatcher(DocumentNameMatcher.MATCHES_NONE); + + ClaimValidator claimValidator = underTest.getClaimValidator(); + + claimValidator.setMax(ClaimStatistic.Counter.APPROVED, 5); + claimValidator.setMin(ClaimStatistic.Counter.APPROVED, 3); + claimValidator.setMax(ClaimStatistic.Counter.ARCHIVES, 10); + claimValidator.setMin(ClaimStatistic.Counter.BINARIES, 4); + + StringWriter stringWriter = new StringWriter(); + underTest.serde().serialize(stringWriter); + + ReportConfiguration actual = new ReportConfiguration(); + actual.serde().deserialize(() -> new ByteArrayInputStream(stringWriter.toString().getBytes(StandardCharsets.UTF_8)), + DocumentName.builder(new File("/rootDir")).build()); + assertSame(actual, underTest); } /** @@ -770,10 +907,26 @@ static class OutputStreamInterceptor extends OutputStream { public void write(int arg0) { throw new UnsupportedOperationException(); } - + @Override public void close() { ++closeCount; } } + + /** + * A reportable that only reports its name. Does no actual work. + */ + static class TestingReportable implements Reportable { + + @Override + public void run(RatReport report) throws RatException { + // does nothing + } + + @Override + public DocumentName name() { + return DocumentName.builder().setBaseName("").setName("TestingReportable").build(); + } + } } diff --git a/apache-rat-core/src/test/java/org/apache/rat/ReporterOptionsProvider.java b/apache-rat-core/src/test/java/org/apache/rat/ReporterOptionsProvider.java index bd9e4fd63..140af48c8 100644 --- a/apache-rat-core/src/test/java/org/apache/rat/ReporterOptionsProvider.java +++ b/apache-rat-core/src/test/java/org/apache/rat/ReporterOptionsProvider.java @@ -20,7 +20,6 @@ import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileInputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; @@ -54,7 +53,7 @@ import org.apache.rat.test.AbstractOptionsProvider; import org.apache.rat.test.utils.OptionFormatter; import org.apache.rat.test.utils.Resources; -import org.apache.rat.testhelpers.FileUtils; +import org.apache.rat.utils.FileUtils; import org.apache.rat.testhelpers.TestingLog; import org.apache.rat.testhelpers.TextUtils; import org.apache.rat.testhelpers.XmlUtils; @@ -65,6 +64,7 @@ import static org.apache.rat.commandline.Arg.HELP_LICENSES; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Fail.fail; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @@ -114,18 +114,26 @@ protected final ReportConfiguration generateConfig(List> return config; } - private File configureRatDir(Option option) { - configureSourceDir(option); - File result = new File(sourceDir, ".rat"); - FileUtils.mkDir(result); - return result; - } - + /** + * Creates the srcDir. + * @param option the name for the srcDir. + */ private void configureSourceDir(Option option) { sourceDir = new File(baseDir, OptionFormatter.getName(option)); FileUtils.mkDir(sourceDir); } + /** + * Creates the option/.rat directory + * @param option the name for the sourceDirectory. + */ + private File configureRatDir(Option option) { + configureSourceDir(option); + File ratDir = new File(sourceDir, ".rat"); + FileUtils.mkDir(ratDir); + return ratDir; + } + private void validateNoArgSetup() throws IOException, RatException { // verify that without args the report is ok. TestingLog log = new TestingLog(); @@ -133,9 +141,9 @@ private void validateNoArgSetup() throws IOException, RatException { try { ReportConfiguration config = generateConfig(Collections.emptyList()); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.execute(); + Reporter.Output output = reporter.execute(); ClaimValidator validator = config.getClaimValidator(); - assertThat(validator.listIssues(claimStatistic)).isEmpty(); + assertThat(validator.listIssues(output.getStatistic())).isEmpty(); } finally { DefaultLog.setInstance(null); } @@ -160,8 +168,8 @@ private void editLicenseTest(final Option option) { FileUtils.delete(resultFile); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.execute(); - assertThat(claimStatistic).isNotNull(); + Reporter.Output output = reporter.execute(); + assertThat(output.getStatistic()).isNotNull(); String contents = String.join("\n", IOUtils.readLines(new FileReader(testFile))); assertThat(contents).isEqualTo("class NoLicense {}"); assertThat(resultFile).exists(); @@ -214,9 +222,9 @@ private void execLicensesDeniedTest(final Option option, final String[] args) { ReportConfiguration config = generateConfig(ImmutablePair.of(option, args)); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.execute(); + Reporter.Output output = reporter.execute(); ClaimValidator validator = config.getClaimValidator(); - assertThat(validator.listIssues(claimStatistic)).containsExactly("UNAPPROVED"); + assertThat(validator.listIssues(output.getStatistic())).containsExactly("UNAPPROVED"); }); } @@ -228,8 +236,7 @@ protected void licensesDeniedTest() { @OptionCollectionTest.TestFunction protected void licensesDeniedFileTest() { Option option = Arg.LICENSES_DENIED_FILE.find("licenses-denied-file"); - File ratDir = configureRatDir(option); - File outputFile = FileUtils.writeFile(ratDir, "licensesDenied.txt", Collections.singletonList("ILLUMOS")); + File outputFile = FileUtils.writeFile(configureRatDir(option), "licensesDenied.txt", Collections.singletonList("ILLUMOS")); execLicensesDeniedTest(option, new String[]{outputFile.getAbsolutePath()}); } @@ -240,17 +247,11 @@ private void noDefaultsTest(final Option option) { "*/\n\n", "class Test {}\n")); validateNoArgSetup(); - ReportConfiguration config = generateConfig(ImmutablePair.of(option, null)); Reporter reporter = new Reporter(config); - try { - reporter.execute(); - fail("Should have thrown exception"); - } catch (RatException e) { - ClaimStatistic claimStatistic = reporter.getClaimsStatistic(); - ClaimValidator validator = config.getClaimValidator(); - assertThat(validator.listIssues(claimStatistic)).containsExactlyInAnyOrder("DOCUMENT_TYPES", "LICENSE_CATEGORIES", "LICENSE_NAMES", "STANDARDS"); - } + assertThatThrownBy(reporter::execute) + .isInstanceOf(RatException.class) + .hasMessageContaining("At least one license must be defined"); }); } @@ -276,16 +277,16 @@ protected void counterMaxTest() { ReportConfiguration config = generateConfig(Collections.emptyList()); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.execute(); + Reporter.Output output = reporter.execute(); ClaimValidator validator = config.getClaimValidator(); - assertThat(validator.listIssues(claimStatistic)).containsExactly("UNAPPROVED"); + assertThat(validator.listIssues(output.getStatistic())).containsExactly("UNAPPROVED"); arg[0] = "Unapproved:1"; config = generateConfig(ImmutablePair.of(option, arg)); reporter = new Reporter(config); - claimStatistic = reporter.execute(); + output = reporter.execute(); validator = config.getClaimValidator(); - assertThat(validator.listIssues(claimStatistic)).isEmpty(); + assertThat(validator.listIssues(output.getStatistic())).isEmpty(); }); } @@ -301,21 +302,26 @@ protected void counterMinTest() { ReportConfiguration config = generateConfig(Collections.emptyList()); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.execute(); + Reporter.Output output = reporter.execute(); ClaimValidator validator = config.getClaimValidator(); - assertThat(validator.listIssues(claimStatistic)).containsExactly("UNAPPROVED"); + assertThat(validator.listIssues(output.getStatistic())).containsExactly("UNAPPROVED"); arg[0] = "Unapproved:1"; config = generateConfig(ImmutablePair.of(option, arg)); reporter = new Reporter(config); - claimStatistic = reporter.execute(); + output = reporter.execute(); validator = config.getClaimValidator(); - assertThat(validator.listIssues(claimStatistic)).isEmpty(); + assertThat(validator.listIssues(output.getStatistic())).isEmpty(); }); } - // exclude tests - private void execExcludeTest(final Option option, final String[] args, final boolean addIgnored) { + /** + * Runs the exclude tests. + * @param option The exclude option to run. + * @param args the arguments for the command line. + * @param includesRatDir @{code true} if the .rat directory was created. + */ + private void execExcludeTest(final Option option, final String[] args, final boolean includesRatDir) { String[] notExcluded = {"notbaz", "well._afile"}; String[] excluded = {"some.foo", "B.bar", "justbaz"}; @@ -329,22 +335,21 @@ private void execExcludeTest(final Option option, final String[] args, final boo ReportConfiguration config = generateConfig(Collections.emptyList()); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(5); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(addIgnored ? 1 : 0); + Reporter.Output output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(5); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(includesRatDir ? 1 : 0); // filter out source config = generateConfig(ImmutablePair.of(option, args)); reporter = new Reporter(config); - claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(addIgnored ? 4 : 3); + output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(includesRatDir ? 4 : 3); }); } private void excludeFileTest(final Option option) { - File ratDir = configureRatDir(option); - File outputFile = FileUtils.writeFile(ratDir, "exclude.txt", Arrays.asList(EXCLUDE_ARGS)); + File outputFile = FileUtils.writeFile(configureRatDir(option), "exclude.txt", Arrays.asList(EXCLUDE_ARGS)); execExcludeTest(option, new String[]{outputFile.getAbsolutePath()}, true); } @@ -381,16 +386,16 @@ protected void inputExcludeSizeTest() { ReportConfiguration config = generateConfig(Collections.emptyList()); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(0); + Reporter.Output output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isZero(); // filter out source config = generateConfig(ImmutablePair.of(option, args)); reporter = new Reporter(config); - claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(1); + output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(1); }); } @@ -415,15 +420,15 @@ protected void inputExcludeStdTest() { ReportConfiguration config = generateConfig(Collections.emptyList()); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(5); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(4); + Reporter.Output output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(5); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(4); config = generateConfig(ImmutablePair.of(option, args)); reporter = new Reporter(config); - claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(4); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(5); + output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(4); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(5); }); } @@ -471,21 +476,26 @@ protected void inputExcludeParsedScmTest() { ReportConfiguration config = generateConfig(Collections.emptyList()); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(11); + Reporter.Output output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(11); // .gitignore is ignored by default as it is hidden but not counted - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(0); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isZero(); config = generateConfig(ImmutablePair.of(option, args)); reporter = new Reporter(config); - claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(8); + output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(8); }); } - // include tests - private void execIncludeTest(final Option option, final String[] args, boolean addIgnored) { + /** + * Runs the include tests. + * @param option The include option to run. + * @param args the arguments for the command line. + * @param includesRatDir @{code true} if the .rat directory was created. + */ + private void execIncludeTest(final Option option, final String[] args, final boolean includesRatDir) { Option excludeOption = Arg.EXCLUDE.option(); String[] notExcluded = {"B.bar", "justbaz", "notbaz"}; String[] excluded = {"some.foo"}; @@ -499,31 +509,31 @@ private void execIncludeTest(final Option option, final String[] args, boolean a ReportConfiguration config = generateConfig(Collections.emptyList()); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(4); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(addIgnored ? 1 : 0); + Reporter.Output output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(4); + + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(includesRatDir ? 1 : 0); // verify exclude removes most files. config = generateConfig(ImmutablePair.of(excludeOption, EXCLUDE_ARGS)); reporter = new Reporter(config); - claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); + output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); // .gitignore is ignored by default as it is hidden but not counted - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(addIgnored ? 4 : 3); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(includesRatDir ? 4 : 3); // verify include pust them back config = generateConfig(ImmutablePair.of(option, args), ImmutablePair.of(excludeOption, EXCLUDE_ARGS)); reporter = new Reporter(config); - claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3); + output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3); // .gitignore is ignored by default as it is hidden but not counted - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(addIgnored ? 2 : 1); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(includesRatDir ? 2 : 1 ); }); } private void includeFileTest(final Option option) { - File ratDir = configureRatDir(option); - File outputFile = FileUtils.writeFile(ratDir, "include.txt", Arrays.asList(INCLUDE_ARGS)); + File outputFile = FileUtils.writeFile(configureRatDir(option), "include.txt", Arrays.asList(INCLUDE_ARGS)); execIncludeTest(option, new String[]{outputFile.getAbsolutePath()}, true); } @@ -569,15 +579,15 @@ protected void inputIncludeStdTest() { ReportConfiguration config = generateConfig(Collections.singletonList(excludes)); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(5); + Reporter.Output output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(5); config = generateConfig(excludes, ImmutablePair.of(option, args)); reporter = new Reporter(config); - claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(7); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(1); + output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(7); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(1); }); } @@ -594,15 +604,15 @@ protected void inputSourceTest() { ReportConfiguration config = generateConfig(); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(0); + Reporter.Output output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isZero(); config = generateConfig(ImmutablePair.of(option, new String[]{inputFile.getAbsolutePath()})); reporter = new Reporter(config); - claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(0); + output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isZero(); }); } @@ -624,18 +634,18 @@ private void execLicenseFamiliesApprovedTest(final Option option, final String[] ReportConfiguration config = addCatzLicense(generateConfig()); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.execute(); + Reporter.Output output = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(0); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isZero(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1); config = addCatzLicense(generateConfig(arg1)); reporter = new Reporter(config); - claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0); + output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isZero(); }); } @@ -664,17 +674,17 @@ private void execLicenseFamiliesDeniedTest(final Option option, final String[] a ReportConfiguration config = generateConfig(); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0); + Reporter.Output output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isZero(); config = generateConfig(arg1); reporter = new Reporter(config); - claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(0); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1); + output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isZero(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1); }); } @@ -704,26 +714,26 @@ private void configTest(final Option option) { ReportConfiguration config = generateConfig(); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1); + Reporter.Output output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1); config = generateConfig(arg1); reporter = new Reporter(config); - claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(2); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0); + output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(2); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isZero(); Pair arg2 = ImmutablePair.of(Arg.CONFIGURATION_NO_DEFAULTS.find("configuration-no-defaults"), null); config = generateConfig(arg1, arg2); reporter = new Reporter(config); - claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1); + output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1); }); } @@ -749,25 +759,24 @@ protected void execLicensesApprovedTest(final Option option, String[] args) { ReportConfiguration config = generateConfig(); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1); + Reporter.Output output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1); config = generateConfig(arg1); reporter = new Reporter(config); - claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(2); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0); + output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(2); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isZero(); }); } @OptionCollectionTest.TestFunction protected void licensesApprovedFileTest() { Option option = Arg.LICENSES_APPROVED_FILE.find("licenses-approved-file"); - File ratDir = configureRatDir(option); - File outputFile = FileUtils.writeFile(ratDir, "licensesApproved.txt", Collections.singletonList("GPL1")); + File outputFile = FileUtils.writeFile(configureRatDir(option), "licensesApproved.txt", Collections.singletonList("GPL1")); execLicensesApprovedTest(option, new String[]{outputFile.getAbsolutePath()}); } @@ -793,17 +802,17 @@ protected void scanHiddenDirectoriesTest() { ReportConfiguration config = generateConfig(); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0); + Reporter.Output output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isZero(); config = generateConfig(arg1); reporter = new Reporter(config); - claimStatistic = reporter.execute(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1); + output = reporter.execute(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1); }); } @@ -817,10 +826,11 @@ private void outTest(final Option option) { ReportConfiguration config = generateConfig(ImmutablePair.of(option, args)); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.output(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0); + Reporter.Output output = reporter.execute(); + output.format(config); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isZero(); String actualText = TextUtils.readFile(outFile); TextUtils.assertContainsExactly(1, "Apache License 2.0: 1 ", actualText); @@ -857,10 +867,11 @@ private void styleSheetTest(final Option option) { args[0] = sheet.arg(); ReportConfiguration config = generateConfig(ImmutablePair.of(option, args)); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.output(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(0); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1); + Reporter.Output output = reporter.execute(); + output.format(config); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isZero(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1); String actualText = baos.toString(StandardCharsets.UTF_8); switch (sheet) { @@ -878,6 +889,9 @@ private void styleSheetTest(final Option option) { case UNAPPROVED_LICENSES: TextUtils.assertContainsExactly(1, "Files with unapproved licenses:" + System.lineSeparator() + " /stylesheet", actualText); break; + case XHTML5: + TextUtils.assertPatternInTarget("Approved<\\/td>\\s+\\d+<\\/td>\\s+A count of approved licenses.<\\/td>", actualText); + break; default: fail("No test for stylesheet " + sheet); break; @@ -887,10 +901,11 @@ private void styleSheetTest(final Option option) { args[0] = file.getAbsolutePath(); ReportConfiguration config = generateConfig(ImmutablePair.of(option, args)); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.output(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(0); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1); + Reporter.Output output = reporter.execute(); + output.format(config); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isZero(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1); String actualText = baos.toString(StandardCharsets.UTF_8); TextUtils.assertContainsExactly(1, "Hello world", actualText); @@ -929,15 +944,16 @@ protected void xmlTest() { ReportConfiguration config = generateConfig(ImmutablePair.of(option, null)); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.output(); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(0); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1); + Reporter.Output output = reporter.execute(); + output.format(config); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isZero(); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1); String actualText = baos.toString(StandardCharsets.UTF_8); TextUtils.assertContainsExactly(1, "", actualText); - try (InputStream expected = StyleSheets.getStyleSheet("xml").ioSupplier().get(); + try (InputStream expected = StyleSheets.getStyleSheet("xml", null).ioSupplier().ioSupplier().get(); InputStream actual = config.getStyleSheet().get()) { assertThat(IOUtils.contentEquals(expected, actual)).as("'xml' does not match").isTrue(); } @@ -960,12 +976,12 @@ protected void logLevelTest() { ReportConfiguration config = generateConfig(); Reporter reporter = new Reporter(config); - reporter.output(); + reporter.execute(); TextUtils.assertNotContains("DEBUG", baos.toString(StandardCharsets.UTF_8)); config = generateConfig(ImmutablePair.of(option, new String[]{"debug"})); reporter = new Reporter(config); - reporter.output(); + reporter.execute(); TextUtils.assertContains("DEBUG", baos.toString(StandardCharsets.UTF_8)); } catch (IOException | RatException e) { fail(e.getMessage(), e); @@ -981,16 +997,11 @@ private void listLicenses(final Option option) { assertDoesNotThrow(() -> { configureSourceDir(option); - File outFile = new File(sourceDir, "out.xml"); - FileUtils.delete(outFile); - ImmutablePair outputFile = ImmutablePair.of(Arg.OUTPUT_FILE.option(), new String[]{outFile.getAbsolutePath()}); - ImmutablePair stylesheet = ImmutablePair.of(Arg.OUTPUT_STYLE.option(), new String[]{StyleSheets.XML.arg()}); for (LicenseSetFactory.LicenseFilter filter : LicenseSetFactory.LicenseFilter.values()) { args[0] = filter.name(); - ReportConfiguration config = generateConfig(outputFile, stylesheet, ImmutablePair.of(option, args)); + ReportConfiguration config = generateConfig(ImmutablePair.of(option, args)); Reporter reporter = new Reporter(config); - reporter.output(); - Document document = XmlUtils.toDom(new FileInputStream(outFile)); + Document document = reporter.execute().getDocument(); switch (filter) { case ALL: XmlUtils.assertIsPresent(filter.name(), document, xPath, "/rat-report/rat-config/licenses/license[@id='AL2.0']"); @@ -1027,16 +1038,12 @@ private void listFamilies(final Option option) { assertDoesNotThrow(() -> { configureSourceDir(option); - File outFile = new File(sourceDir, "out.xml"); - FileUtils.delete(outFile); - ImmutablePair outputFile = ImmutablePair.of(Arg.OUTPUT_FILE.option(), new String[]{outFile.getAbsolutePath()}); - ImmutablePair stylesheet = ImmutablePair.of(Arg.OUTPUT_STYLE.option(), new String[]{StyleSheets.XML.arg()}); for (LicenseSetFactory.LicenseFilter filter : LicenseSetFactory.LicenseFilter.values()) { args[0] = filter.name(); - ReportConfiguration config = generateConfig(outputFile, stylesheet, ImmutablePair.of(option, args)); + ReportConfiguration config = generateConfig(ImmutablePair.of(option, args)); Reporter reporter = new Reporter(config); - reporter.output(); - Document document = XmlUtils.toDom(Files.newInputStream(outFile.toPath())); + Reporter.Output output = reporter.execute(); + Document document = output.getDocument(); switch (filter) { case ALL: XmlUtils.assertIsPresent(filter.name(), document, xPath, "/rat-report/rat-config/families/family[@id='AL']"); @@ -1088,9 +1095,8 @@ private void archiveTest(final Option option) { args[0] = proc.name(); ReportConfiguration config = generateConfig(outputFile, stylesheet, ImmutablePair.of(option, args)); Reporter reporter = new Reporter(config); - reporter.output(); - Document document = XmlUtils.toDom(Files.newInputStream(outFile.toPath())); + Document document = reporter.execute().getDocument(); XmlUtils.assertIsPresent(proc.name(), document, xPath, "/rat-report/resource[@name='/dummy.jar']"); switch (proc) { case ABSENCE: @@ -1138,9 +1144,9 @@ private void standardTest(final Option option) { args[0] = proc.name(); ReportConfiguration config = generateConfig(outputFile, stylesheet, ImmutablePair.of(option, args)); Reporter reporter = new Reporter(config); - reporter.output(); + Reporter.Output output = reporter.execute(); - Document document = XmlUtils.toDom(Files.newInputStream(outFile.toPath())); + Document document = output.getDocument(); XmlUtils.assertIsPresent(proc.name(), document, xPath, testDoc); XmlUtils.assertIsPresent(proc.name(), document, xPath, missingDoc); diff --git a/apache-rat-core/src/test/java/org/apache/rat/ReporterOptionsTest.java b/apache-rat-core/src/test/java/org/apache/rat/ReporterOptionsTest.java index ccb82f3c6..4f52a31a5 100644 --- a/apache-rat-core/src/test/java/org/apache/rat/ReporterOptionsTest.java +++ b/apache-rat-core/src/test/java/org/apache/rat/ReporterOptionsTest.java @@ -27,7 +27,7 @@ import org.apache.rat.api.RatException; import org.apache.rat.report.claim.ClaimStatistic; import org.apache.rat.test.AbstractConfigurationOptionsProvider; -import org.apache.rat.testhelpers.FileUtils; +import org.apache.rat.utils.FileUtils; import org.apache.rat.testhelpers.XmlUtils; import org.apache.rat.utils.DefaultLog; import org.apache.rat.utils.Log; @@ -78,14 +78,14 @@ void testRat362() { FileUtils.writeFile(testDir, "foo.md"); ReportConfiguration config = OptionCollection.parseCommands(testDir, args, o -> fail("Help called"), true); Reporter reporter = new Reporter(config); - ClaimStatistic claimStatistic = reporter.execute(); - XmlUtils.printDocument(System.out, reporter.getDocument()); + Reporter.Output output = reporter.execute(); + XmlUtils.printDocument(System.out, output.getDocument()); XPath xpath = XPathFactory.newInstance().newXPath(); - XmlUtils.assertIsPresent(reporter.getDocument(), xpath, "/rat-report/resource[@name='/foo.md']"); - XmlUtils.assertAttributes(reporter.getDocument(), xpath, "/rat-report/resource[@name='/foo.md']", + XmlUtils.assertIsPresent(output.getDocument(), xpath, "/rat-report/resource[@name='/foo.md']"); + XmlUtils.assertAttributes(output.getDocument(), xpath, "/rat-report/resource[@name='/foo.md']", XmlUtils.mapOf("type", "IGNORED")); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(0); - assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(2); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(0); + assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(2); } catch (IOException | RatException | XPathExpressionException e) { fail(e); } diff --git a/apache-rat-core/src/test/java/org/apache/rat/ReporterTest.java b/apache-rat-core/src/test/java/org/apache/rat/ReporterTest.java index d9f11b7ad..ba457fd50 100644 --- a/apache-rat-core/src/test/java/org/apache/rat/ReporterTest.java +++ b/apache-rat-core/src/test/java/org/apache/rat/ReporterTest.java @@ -7,7 +7,7 @@ * "License"); you may not use this file except in compliance * * with the License. You may obtain a copy of the License at * * * - * http://www.apache.org/licenses/LICENSE-2.0 * + * https://www.apache.org/licenses/LICENSE-2.0 * * * * Unless required by applicable law or agreed to in writing, * * software distributed under the License is distributed on an * @@ -19,23 +19,31 @@ package org.apache.rat; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Fail.fail; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.IOException; import java.io.PrintStream; +import java.lang.reflect.Method; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.TreeMap; +import java.util.UUID; +import java.util.stream.Stream; import javax.xml.XMLConstants; import javax.xml.transform.Source; +import javax.xml.transform.TransformerException; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; @@ -45,25 +53,33 @@ import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathFactory; -import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.DefaultParser; -import org.apache.commons.cli.ParseException; import org.apache.commons.io.FileUtils; import org.apache.rat.api.Document.Type; import org.apache.rat.api.RatException; +import org.apache.rat.commandline.Arg; import org.apache.rat.commandline.ArgumentContext; -import org.apache.rat.commandline.StyleSheets; import org.apache.rat.document.FileDocument; import org.apache.rat.document.DocumentName; import org.apache.rat.license.ILicenseFamily; -import org.apache.rat.license.LicenseSetFactory; import org.apache.rat.report.claim.ClaimStatistic; +import org.apache.rat.report.claim.ClaimStatisticTest; import org.apache.rat.test.utils.Resources; +import org.apache.rat.testhelpers.BaseOptionCollection; import org.apache.rat.testhelpers.TextUtils; import org.apache.rat.testhelpers.XmlUtils; +import org.apache.rat.testhelpers.data.ReportTestDataProvider; +import org.apache.rat.testhelpers.data.TestData; +import org.apache.rat.testhelpers.data.ValidatorData; +import org.apache.rat.utils.StandardXmlFactory; import org.apache.rat.walker.DirectoryWalker; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.w3c.dom.Document; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; @@ -72,22 +88,62 @@ * Tests the output of the Reporter. */ public class ReporterTest { - @TempDir - File tempDirectory; + /** + * temporary file. Not using tempDir because it does not + * always work. + */ + private static Path tempPath; + + /** + * The directory for the test. + */ + private Path testPath; + + /** + * Directory for the test data. + */ final String basedir; + private final OptionCollectionParser collectionParser; + ReporterTest() throws URISyntaxException { basedir = Resources.getExampleResource("exampleData").getPath(); + collectionParser = new OptionCollectionParser(BaseOptionCollection.builder().build()); } - @Test - void testExecute() throws RatException, ParseException { - File output = new File(tempDirectory, "testExecute"); + @BeforeAll + static void setUp() throws IOException { + tempPath = Files.createTempDirectory("ReporterTest"); + } - CommandLine cl = new DefaultParser().parse(OptionCollection.buildOptions(), new String[]{"--output-style", "xml", "--output-file", output.getPath(), basedir}); - ArgumentContext ctxt = new ArgumentContext(new File("."), cl); - ReportConfiguration config = OptionCollection.createConfiguration(ctxt); - ClaimStatistic statistic = new Reporter(config).execute(); + @AfterAll + static void cleanup() throws IOException { + FileUtils.deleteDirectory(tempPath.toFile()); + } + + @BeforeEach + void setUpTest(TestInfo testInfo) { + Optional testMethod = testInfo.getTestMethod(); + this.testPath = tempPath.resolve(testMethod.isPresent() ? + testMethod.get().getName() : + UUID.randomUUID().toString()); + File file = testPath.toFile(); + if (!file.exists()) { + if (!file.mkdirs()) { + throw new RuntimeException("Unable to create temp directory: " + file.getName()); + } + } else { + if (!file.isDirectory()) { + throw new RuntimeException(file.getName() + " is NOT a directory."); + } + } + } + + @Test + void testExecute() throws RatException { + File output = testPath.resolve("output.xml").toFile(); + ArgumentContext ctxt = collectionParser.parseCommands(new File("."), new String[]{"--output-style", "xml", "--output-file", output.getPath(), basedir}); + ClaimStatistic statistic = new Reporter(ctxt.getConfiguration()).execute().getStatistic(); assertThat(statistic.getCounter(Type.ARCHIVE)).isEqualTo(1); assertThat(statistic.getCounter(Type.BINARY)).isEqualTo(2); @@ -136,13 +192,20 @@ void testExecute() throws RatException, ParseException { } @Test - void testOutputOption() throws Exception { - File output = new File(tempDirectory, "test"); - CommandLine commandLine = new DefaultParser().parse(OptionCollection.buildOptions(), new String[]{"-o", output.getCanonicalPath(), basedir}); - ArgumentContext ctxt = new ArgumentContext(new File("."), commandLine); - + void testExecuteNoSource() throws ParseException, RatException, TransformerException { + File output = testPath.resolve("testExecuteNoSource").toFile(); + ArgumentContext ctxt = collectionParser.parseCommands(new File("."), new String[]{"--output-style", "xml", "--output-file", output.getPath()}); ReportConfiguration config = OptionCollection.createConfiguration(ctxt); - new Reporter(config).output(); + Reporter.Output result = new Reporter(config).execute(); + assertThat(StandardXmlFactory.serializeDocument(result.getDocument())).isEmpty(); + ClaimStatisticTest.assertSame(result.getStatistic(), new ClaimStatistic()); + } + + @Test + void testOutputOption() throws Exception { + File output = testPath.resolve("output.txt").toFile(); + ArgumentContext ctxt = collectionParser.parseCommands(new File("."), new String[]{"--output-file", output.getCanonicalPath(), basedir}); + new Reporter(ctxt.getConfiguration()).execute().format(ctxt.getConfiguration()); assertThat(output.exists()).isTrue(); String content = FileUtils.readFileToString(output, StandardCharsets.UTF_8); TextUtils.assertPatternInTarget("^! Unapproved:\\s*2 ", content); @@ -150,18 +213,25 @@ void testOutputOption() throws Exception { assertThat(content).contains("/sub/Empty.txt"); } + @Test + void testGetOutputMethod() throws Exception { + File output = testPath.resolve("test").toFile(); + ArgumentContext ctxt = collectionParser.parseCommands(new File("."), new String[]{"--output-file", output.getCanonicalPath(), basedir}); + ReportConfiguration config = OptionCollection.createConfiguration(ctxt); + Reporter reporter = new Reporter(config); + Reporter.Output expected = reporter.execute(); + assertThat(reporter.getOutput()).isEqualTo(expected); + } + @Test void testDefaultOutput() throws Exception { - File output = new File(tempDirectory, "testDefaultOutput"); + File output = testPath.resolve("captured.txt").toFile(); PrintStream origin = System.out; try (PrintStream out = new PrintStream(output)) { System.setOut(out); - CommandLine commandLine = new DefaultParser().parse(OptionCollection.buildOptions(), new String[]{basedir}); - ArgumentContext ctxt = new ArgumentContext(new File("."), commandLine); - - ReportConfiguration config = OptionCollection.createConfiguration(ctxt); - new Reporter(config).output(); + ArgumentContext ctxt = collectionParser.parseCommands(new File("."), new String[]{basedir}); + new Reporter(ctxt.getConfiguration()).execute().format(ctxt.getConfiguration()); } finally { System.setOut(origin); } @@ -209,13 +279,10 @@ void testXMLOutput() throws Exception { expected.put("/tri.txt", mapOf("encoding", "ISO-8859-1", "mediaType", "text/plain", "type", "STANDARD")); - File output = new File(tempDirectory, ".rat/testXMLOutput"); + File output = testPath.resolve(".rat/testXMLOutput").toFile(); output.getParentFile().mkdir(); - CommandLine commandLine = new DefaultParser().parse(OptionCollection.buildOptions(), new String[]{"--output-style", "xml", "--output-file", output.getPath(), basedir}); - ArgumentContext ctxt = new ArgumentContext(tempDirectory, commandLine); - - ReportConfiguration config = OptionCollection.createConfiguration(ctxt); - new Reporter(config).output(); + ArgumentContext ctxt = collectionParser.parseCommands(testPath.toFile(), new String[]{"--output-style", "xml", "--output-file", output.getPath(), basedir}); + new Reporter(ctxt.getConfiguration()).execute().format(ctxt.getConfiguration()); assertThat(output).exists(); Document doc = XmlUtils.toDom(java.nio.file.Files.newInputStream(output.toPath())); @@ -405,13 +472,8 @@ private Validator initValidator() throws SAXException { @Test void xmlReportTest() throws Exception { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ReportConfiguration configuration = initializeConfiguration(); - configuration.setStyleSheet(StyleSheets.XML.getStyleSheet()); - configuration.setOut(new ReportConfiguration.IODescriptor("xmlReportTest", () -> out)); - new Reporter(configuration).output(); - Document doc = XmlUtils.toDom(new ByteArrayInputStream(out.toByteArray())); + Document doc = new Reporter(configuration).execute().getDocument(); XPath xPath = XPathFactory.newInstance().newXPath(); @@ -457,10 +519,9 @@ void plainReportTest() throws Exception { "Generated at: "; ByteArrayOutputStream out = new ByteArrayOutputStream(); ReportConfiguration configuration = initializeConfiguration(); - configuration.setOut(new ReportConfiguration.IODescriptor("plainReportTest", () -> out)); - new Reporter(configuration).output(); + configuration.setOut(new ReportConfiguration.IODescriptor<>("plainReportTest", () -> out)); + new Reporter(configuration).execute().format(configuration); - out.flush(); String document = out.toString(); TextUtils.assertNotContains("", document); @@ -473,11 +534,10 @@ void plainReportTest() throws Exception { void unapprovedLicensesReportTest() throws Exception { ByteArrayOutputStream out = new ByteArrayOutputStream(); ReportConfiguration configuration = initializeConfiguration(); - configuration.setOut(new ReportConfiguration.IODescriptor("unapprovedLicensesReportTest", () -> out)); + configuration.setOut(new ReportConfiguration.IODescriptor<>("unapprovedLicensesReportTest", () -> out)); configuration.setStyleSheet(this.getClass().getResource("/org/apache/rat/unapproved-licenses.xsl")); - new Reporter(configuration).output(); + new Reporter(configuration).execute().format(configuration); - out.flush(); String document = out.toString(); TextUtils.assertContains("Generated at: ", document ); @@ -489,44 +549,72 @@ void unapprovedLicensesReportTest() throws Exception { void listLicensesReportTest() throws Exception { ByteArrayOutputStream out = new ByteArrayOutputStream(); ReportConfiguration configuration = initializeConfiguration(); - configuration.setOut(new ReportConfiguration.IODescriptor("listLicensesReportTest", () -> out)); + configuration.setOut(new ReportConfiguration.IODescriptor<>("listLicensesReportTest", () -> out)); configuration.setStyleSheet(StyleSheets.UNAPPROVED_LICENSES.getStyleSheet()); Reporter.listLicenses(configuration, LicenseSetFactory.LicenseFilter.NONE); out.flush(); String document = out.toString(); - assertThat(document).contains("Licenses (NONE):"); } @Test void counterMaxTest() throws Exception { ReportConfiguration config = initializeConfiguration(); - Reporter reporter = new Reporter(config); - reporter.output(); + Reporter.Output output = new Reporter(config).execute(); assertThat(config.getClaimValidator().hasErrors()).isTrue(); - assertThat(config.getClaimValidator().isValid(ClaimStatistic.Counter.UNAPPROVED, reporter.getClaimsStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED))) + assertThat(config.getClaimValidator().isValid(ClaimStatistic.Counter.UNAPPROVED, output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED))) .isFalse(); config = initializeConfiguration(); config.getClaimValidator().setMax(ClaimStatistic.Counter.UNAPPROVED, 2); - reporter = new Reporter(config); - reporter.output(); + output = new Reporter(config).execute(); assertThat(config.getClaimValidator().hasErrors()).isFalse(); - assertThat(config.getClaimValidator().isValid(ClaimStatistic.Counter.UNAPPROVED, reporter.getClaimsStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED))) + assertThat(config.getClaimValidator().isValid(ClaimStatistic.Counter.UNAPPROVED, output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED))) .isTrue(); } + static Stream getTestData() { + BaseOptionCollection.Builder builder = BaseOptionCollection.builder() + .unsupported(Arg.OUTPUT_FILE); + return new ReportTestDataProvider().getOptionTests(builder.build()).stream().map(testData -> + Arguments.of(testData.getTestName(), testData)); + } + + @ParameterizedTest( name = "{index} {0}") + @MethodSource("getTestData") + void testReportData(String name, TestData test) throws Exception { + Path invokePath = testPath.resolve(test.getTestName()); + org.apache.rat.utils.FileUtils.mkDir(invokePath.toFile()); + + test.setupFiles(invokePath); + ArgumentContext ctxt = collectionParser.parseCommands(invokePath.toFile(), + test.getCommandLine(invokePath.toString())); + if (test.expectingException()) { + assertThatThrownBy(() -> new Reporter(ctxt.getConfiguration()).execute()).as("Expected throws from " + name) + .hasMessageContaining(test.getExpectedException().getMessage()); + ValidatorData data = new ValidatorData(Reporter.Output.builder().configuration(ctxt.getConfiguration()).build(), + invokePath.toString()); + test.getValidator().accept(data); + } else { + Reporter.Output output = ctxt.getConfiguration() != null ? new Reporter(ctxt.getConfiguration()).execute() : + Reporter.Output.builder().build(); + ValidatorData data = new ValidatorData(output, invokePath.toString()); + data.getOutput().format(data.getConfiguration()); + test.getValidator().accept(data); + } + } + private record LicenseInfo(String id, String family, boolean approval, boolean hasNotes) { - LicenseInfo(String id, boolean approval, boolean hasNotes) { - this(id, id, approval, hasNotes); - } + LicenseInfo(String id, boolean approval, boolean hasNotes) { + this(id, id, approval, hasNotes); + } - private LicenseInfo(String id, String family, boolean approval, boolean hasNotes) { - this.id = id; - this.family = ILicenseFamily.makeCategory(family); - this.approval = approval; - this.hasNotes = hasNotes; - } + private LicenseInfo(String id, String family, boolean approval, boolean hasNotes) { + this.id = id; + this.family = ILicenseFamily.makeCategory(family); + this.approval = approval; + this.hasNotes = hasNotes; } + } } diff --git a/apache-rat-core/src/test/java/org/apache/rat/analysis/HeaderCheckWorkerTest.java b/apache-rat-core/src/test/java/org/apache/rat/analysis/HeaderCheckWorkerTest.java index 9583f47ed..a2cff0121 100644 --- a/apache-rat-core/src/test/java/org/apache/rat/analysis/HeaderCheckWorkerTest.java +++ b/apache-rat-core/src/test/java/org/apache/rat/analysis/HeaderCheckWorkerTest.java @@ -40,11 +40,10 @@ class HeaderCheckWorkerTest { - /** - * Create an unmodifiable sorted set from members - */ + /** Create an unmodifiable sorted set from members */ private UnmodifiableSortedSet asLicenses(ILicense... licenses) { - SortedSet inner = new TreeSet<>(List.of(licenses)); + SortedSet inner = new TreeSet<>(); + inner.addAll(List.of(licenses)); return (UnmodifiableSortedSet) UnmodifiableSortedSet.unmodifiableSortedSet(inner); } diff --git a/apache-rat-core/src/test/java/org/apache/rat/commandline/ArgTests.java b/apache-rat-core/src/test/java/org/apache/rat/commandline/ArgTests.java index 742632c5a..bd8affac4 100644 --- a/apache-rat-core/src/test/java/org/apache/rat/commandline/ArgTests.java +++ b/apache-rat-core/src/test/java/org/apache/rat/commandline/ArgTests.java @@ -18,13 +18,8 @@ */ package org.apache.rat.commandline; -import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.DefaultParser; -import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.apache.rat.CLIOptionCollection; -import org.apache.rat.DeprecationReporter; -import org.apache.rat.OptionCollection; import org.apache.rat.ReportConfiguration; import org.apache.rat.document.DocumentName; import org.junit.jupiter.params.ParameterizedTest; @@ -37,12 +32,6 @@ public class ArgTests { - private CommandLine createCommandLine(String[] args) throws ParseException { - Options opts = OptionCollection.buildOptions(); - return DefaultParser.builder().setDeprecatedHandler(DeprecationReporter.getLogReporter()) - .setAllowPartialMatching(true).build().parse(opts, args); - } - @ParameterizedTest(name = "{0}") @ValueSource(strings = { "rat.txt", "./rat.txt", "/rat.txt", "target/rat.test" }) public void outputFileNameNoDirectoryTest(String name) throws ParseException { @@ -62,9 +51,8 @@ public void setOut(File file) { Path workingPath = localFile.getAbsoluteFile().toPath(); String expected = fsInfo.normalize(workingPath.resolve("./" + fileName).toString()); - CommandLine commandLine = createCommandLine(new String[]{"--output-file", fileName}); OutputFileConfig configuration = new OutputFileConfig(); - ArgumentContext ctxt = new ArgumentContext(localFile, configuration, commandLine); + ArgumentContext ctxt = new ArgumentContext(localFile, configuration, CLIOptionCollection.INSTANCE.getOptions(), new String[]{"--output-file", fileName}); Arg.processArgs(ctxt, CLIOptionCollection.INSTANCE); if (name.equals("/rat.txt")) { assertThat(fsInfo.normalize(configuration.actual.getAbsolutePath())).isEqualTo(localFileName.getRoot() + "rat.txt"); diff --git a/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/ExclusionProcessorTest.java b/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/ExclusionProcessorTest.java index 19e2e1542..cf9d1c795 100644 --- a/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/ExclusionProcessorTest.java +++ b/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/ExclusionProcessorTest.java @@ -18,6 +18,9 @@ */ package org.apache.rat.config.exclusion; +import java.io.ByteArrayInputStream; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; import java.nio.file.FileSystems; import java.nio.file.Path; import java.util.ArrayList; @@ -26,6 +29,9 @@ import org.apache.commons.io.FileUtils; import org.apache.rat.document.DocumentNameMatcher; import org.apache.rat.document.DocumentName; +import org.apache.rat.report.xml.writer.XmlWriter; +import org.apache.rat.utils.StandardXmlFactory; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.io.File; @@ -36,7 +42,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.Mockito; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; import static java.lang.String.format; import static org.assertj.core.api.Assertions.assertThat; @@ -44,6 +51,8 @@ import static org.apache.rat.document.FSInfoTest.UNIX; import static org.apache.rat.document.FSInfoTest.WINDOWS; import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; public class ExclusionProcessorTest { @@ -62,7 +71,7 @@ private String dump(DocumentNameMatcher nameMatcher, DocumentName name) { private DocumentName mkName(DocumentName baseDir, String pth) throws IOException { DocumentName result = baseDir.resolve(ExclusionUtils.convertSeparator(pth, "/", baseDir.getDirectorySeparator())); - DocumentName mocked = Mockito.spy(result); + DocumentName mocked = spy(result); String fn = result.localized(FileSystems.getDefault().getSeparator()); File file = tempDir.resolve(fn.substring(1)).toFile(); @@ -83,7 +92,7 @@ private DocumentName mkName(DocumentName baseDir, String pth) throws IOException if (!file.createNewFile()) { fail(() -> "Unable to create file: " + file); } - Mockito.when(mocked.asFile()).thenReturn(file); + when(mocked.asFile()).thenReturn(file); return mocked; } @@ -264,4 +273,218 @@ private static Stream getDocumentNames() { return lst.stream(); } + + @Test + void addNullIncludedMatcherTest() { + ExclusionProcessor underTest = new ExclusionProcessor(); + assertThat(underTest.getIncludedPaths()).isEmpty(); + underTest.addIncludedMatcher(null); + assertThat(underTest.getIncludedPaths()).isEmpty(); + underTest.addIncludedMatcher(DocumentNameMatcher.MATCHES_ALL) + .addIncludedMatcher(null); + assertThat(underTest.getIncludedPaths()).hasSize(1); + List actualPaths = underTest.getIncludedPaths().stream().map(DocumentNameMatcher::toString).toList(); + assertThat(actualPaths).containsExactly(DocumentNameMatcher.MATCHES_ALL.toString()); + } + + @Test + void addNullExcludedMatcherTest() { + ExclusionProcessor underTest = new ExclusionProcessor(); + assertThat(underTest.getExcludedPaths()).isEmpty(); + underTest.addExcludedMatcher(null); + assertThat(underTest.getExcludedPaths()).isEmpty(); + underTest.addExcludedMatcher(DocumentNameMatcher.MATCHES_ALL) + .addExcludedMatcher(null); + assertThat(underTest.getExcludedPaths()).hasSize(1); + List actualPaths = underTest.getExcludedPaths().stream().map(DocumentNameMatcher::toString).toList(); + assertThat(actualPaths).containsExactly(DocumentNameMatcher.MATCHES_ALL.toString()); + } + + @Test + void addFileProcessor() { + ExclusionProcessor underTest = new ExclusionProcessor(); + assertThat(underTest.getFileProcessors()).isEmpty(); + underTest.addFileProcessor(null); + assertThat(underTest.getFileProcessors()).isEmpty(); + underTest.addFileProcessor(StandardCollection.HIDDEN_FILE) + .addFileProcessor(null); + assertThat(underTest.getFileProcessors()).containsExactly(StandardCollection.HIDDEN_FILE); + } + + @Test + void addNullIncludedPatternTest() { + ExclusionProcessor underTest = new ExclusionProcessor(); + assertThat(underTest.getIncludedPatterns()).isEmpty(); + underTest.addIncludedPatterns(null); + assertThat(underTest.getIncludedPatterns()).isEmpty(); + underTest.addIncludedPatterns(Collections.emptyList()); + assertThat(underTest.getIncludedPatterns()).isEmpty(); + underTest.addIncludedPatterns(List.of("hello/world")) + .addIncludedPatterns(null) + .addIncludedPatterns(Collections.emptyList()); + assertThat(underTest.getIncludedPatterns()).containsExactly("hello/world"); + } + + @Test + void addNullIncludedCollectionTest() { + ExclusionProcessor underTest = new ExclusionProcessor(); + assertThat(underTest.getIncludedCollections()).isEmpty(); + underTest.addIncludedCollection(null); + assertThat(underTest.getIncludedCollections()).isEmpty(); + underTest.addIncludedCollection(StandardCollection.HIDDEN_FILE) + .addIncludedCollection(null); + assertThat(underTest.getIncludedCollections()).containsExactly(StandardCollection.HIDDEN_FILE); + } + + @Test + void addNullExcludedCollectionTest() { + ExclusionProcessor underTest = new ExclusionProcessor(); + assertThat(underTest.getExcludedCollections()).isEmpty(); + underTest.addExcludedCollection(null); + assertThat(underTest.getExcludedCollections()).isEmpty(); + underTest.addExcludedCollection(StandardCollection.HIDDEN_FILE) + .addExcludedCollection(null); + assertThat(underTest.getExcludedCollections()).containsExactly(StandardCollection.HIDDEN_FILE); + } + + @Test + void addNullExcludedPatternTest() { + ExclusionProcessor underTest = new ExclusionProcessor(); + assertThat(underTest.getExcludedPatterns()).isEmpty(); + underTest.addExcludedPatterns(null); + assertThat(underTest.getExcludedPatterns()).isEmpty(); + underTest.addExcludedPatterns(Collections.emptyList()); + assertThat(underTest.getExcludedPatterns()).isEmpty(); + underTest.addExcludedPatterns(List.of("hello/world")) + .addExcludedPatterns(null) + .addExcludedPatterns(Collections.emptyList()); + assertThat(underTest.getExcludedPatterns()).containsExactly("hello/world"); + } + + public static void assertSame(ExclusionProcessor actual, ExclusionProcessor expected) { + assertThat(actual.getExcludedCollections()).containsExactlyElementsOf(expected.getExcludedCollections()); + assertThat(actual.getFileProcessors()).containsExactlyElementsOf(expected.getFileProcessors()); + assertThat(actual.getIncludedCollections()).containsExactlyElementsOf(expected.getIncludedCollections()); + assertThat(actual.getExcludedPatterns()).containsExactlyElementsOf(expected.getExcludedPatterns()); + assertThat(actual.getIncludedPatterns()).containsExactlyElementsOf(expected.getIncludedPatterns()); + + // paths only match on name. deserialized paths are not functional. + List actualPaths = actual.getExcludedPaths().stream().map(DocumentNameMatcher::toString).toList(); + List expectedPaths = expected.getExcludedPaths().stream().map(DocumentNameMatcher::toString).toList(); + assertThat(actualPaths).as("excluded paths").containsExactlyElementsOf(expectedPaths); + + actualPaths = actual.getIncludedPaths().stream().map(DocumentNameMatcher::toString).toList(); + expectedPaths = expected.getIncludedPaths().stream().map(DocumentNameMatcher::toString).toList(); + assertThat(actualPaths).as("included paths").containsExactlyElementsOf(expectedPaths); + } + + @ParameterizedTest + @MethodSource("serdeTestData") + void serdeTest(ExclusionProcessor underTest) throws IOException, SAXException { + StringWriter stringWriter = new StringWriter(); + try (XmlWriter writer = new XmlWriter(stringWriter)) { + underTest.serde().serialize(writer); + } + Document document = StandardXmlFactory.documentBuilder().parse(new ByteArrayInputStream(stringWriter.toString().getBytes(StandardCharsets.UTF_8))); + ExclusionProcessor actual = new ExclusionProcessor(); + actual.serde().deserialize(document.getElementsByTagName("ExclusionProcessor").item(0)); + assertSame(actual, underTest); + } + + static List serdeTestData() { + List tests = new ArrayList<>(); + tests.add(new ExclusionProcessor()); + + // single exclusions + tests.add(new ExclusionProcessor() + .addExcludedPatterns(List.of("pattern/**"))); + + tests.add(new ExclusionProcessor() + .addExcludedCollection(StandardCollection.BAZAAR)); + + + tests.add(new ExclusionProcessor() + .addExcludedMatcher(DocumentNameMatcher.MATCHES_ALL)); + + // single inclusions + tests.add(new ExclusionProcessor() + .addIncludedPatterns(List.of("pattern/**"))); + + tests.add(new ExclusionProcessor() + .addIncludedCollection(StandardCollection.BAZAAR)); + + tests.add(new ExclusionProcessor() + .addIncludedMatcher(DocumentNameMatcher.MATCHES_NONE)); + + // full population + tests.add(new ExclusionProcessor() + .addExcludedPatterns(List.of("pattern/**", "pattern2/**")) + .addExcludedCollection(StandardCollection.BAZAAR) + .addExcludedCollection(StandardCollection.MISC) + .addExcludedMatcher(DocumentNameMatcher.MATCHES_ALL) + .addIncludedPatterns(List.of("**/pattern3", "**/pattern4")) + .addIncludedCollection(StandardCollection.ARCH) + .addIncludedCollection(StandardCollection.BITKEEPER) + .addIncludedMatcher(DocumentNameMatcher.MATCHES_NONE)); + + return tests; + } + + @Test + void reportExclusionsTest() throws IOException { + ExclusionProcessor underTest = new ExclusionProcessor() + .addExcludedPatterns(List.of("pattern/**", "pattern2/**")) + .addExcludedCollection(StandardCollection.BAZAAR) + .addExcludedCollection(StandardCollection.MISC) + .addFileProcessor(StandardCollection.HIDDEN_FILE) + .addExcludedMatcher(DocumentNameMatcher.MATCHES_ALL) + .addIncludedPatterns(List.of("**/pattern3", "**/pattern4")) + .addIncludedCollection(StandardCollection.ARCH) + .addIncludedCollection(StandardCollection.BITKEEPER) + .addIncludedMatcher(DocumentNameMatcher.MATCHES_NONE); + + StringWriter writer = new StringWriter(); + underTest.reportExclusions(writer); + String actual = writer.toString(); + + assertThat(actual).containsPattern("Excluding patterns:[^$]+\\Qpattern/**\\E") + .containsPattern("Excluding patterns:[^$]+\\Qpattern2/**\\E") + .containsPattern("Including patterns:[^$]+\\Q**/pattern3\\E") + .containsPattern("Including patterns:[^$]+\\Q**/pattern4\\E") + .contains("Excluding " + StandardCollection.BAZAAR + " collection.") + .contains("Excluding " + StandardCollection.MISC + " collection.") + .contains("Including " + StandardCollection.ARCH + " collection.") + .contains("Including " + StandardCollection.BITKEEPER + " collection.") + .contains("Processing exclude file from " + StandardCollection.HIDDEN_FILE) + .contains("Excluding " + DocumentNameMatcher.MATCHES_ALL + ".") + .contains("Including " + DocumentNameMatcher.MATCHES_NONE + "."); + } + + @Test + void getNameMatcherTest() { + ExclusionProcessor underTest = new ExclusionProcessor() + .addExcludedPatterns(List.of("pattern/**", "pattern2/**")) + .addExcludedCollection(StandardCollection.BAZAAR) + .addExcludedCollection(StandardCollection.MISC) + .addFileProcessor(StandardCollection.HIDDEN_FILE) + .addExcludedMatcher(DocumentNameMatcher.MATCHES_ALL) + .addIncludedPatterns(List.of("**/pattern3", "**/pattern4")) + .addIncludedCollection(StandardCollection.ARCH) + .addIncludedCollection(StandardCollection.BITKEEPER) + .addIncludedMatcher(DocumentNameMatcher.MATCHES_NONE); + + assertThat(underTest.getLastMatcherBaseDir()).isNull(); + assertThat(underTest.getLastMatcher()).isNull(); + DocumentName base = DocumentName.builder(new File(".")).build(); + + DocumentNameMatcher matcher = underTest.getNameMatcher(base); + assertThat(underTest.getLastMatcherBaseDir()).isEqualTo(base); + assertThat(underTest.getLastMatcher()).isEqualTo(matcher); + + DocumentNameMatcher matcher2 = underTest.getNameMatcher(base); + assertThat(underTest.getLastMatcherBaseDir()).isEqualTo(base); + assertThat(underTest.getLastMatcher()).isEqualTo(matcher); + assertThat(underTest.getLastMatcherBaseDir()).isEqualTo(base); + assertThat(matcher2).isEqualTo(matcher); + } } diff --git a/apache-rat-core/src/test/java/org/apache/rat/configuration/XMLConfigurationReaderTest.java b/apache-rat-core/src/test/java/org/apache/rat/configuration/XMLConfigurationReaderTest.java index f0779b31f..10c9759fb 100644 --- a/apache-rat-core/src/test/java/org/apache/rat/configuration/XMLConfigurationReaderTest.java +++ b/apache-rat-core/src/test/java/org/apache/rat/configuration/XMLConfigurationReaderTest.java @@ -52,6 +52,10 @@ public class XMLConfigurationReaderTest { public static final String[] EXPECTED_LICENSES = {"AL1.0", "AL1.1", "AL2.0", "BSD-3", "DOJO", "TMF", "CDDL1", "ILLUMOS", "GPL1", "GPL2", "GPL3", "MIT", "OASIS", "W3C", "W3CD"}; + public static final String[] APPROVED_LICENSES = { "AL1.0", "AL1.1", "AL2.0", "BSD-3", "DOJO", "TMF", "CDDL1", "ILLUMOS", + "MIT", "OASIS", "W3C", "W3CD" }; + + @Test void approvedLicenseIdTest() throws URISyntaxException { XMLConfigurationReader reader = new XMLConfigurationReader(); @@ -59,9 +63,8 @@ void approvedLicenseIdTest() throws URISyntaxException { assertThat(url).isNotNull(); reader.read(url.toURI()); - Collection readCategories = reader.approvedLicenseId(); - assertThat(readCategories.toArray(new String[readCategories.size()])) - .containsExactly(APPROVED_IDS); + Collection actual = reader.approvedLicenseId(); + assertThat(actual).containsExactlyInAnyOrder(APPROVED_IDS); } @Test @@ -77,10 +80,12 @@ void LicensesTest() throws URISyntaxException { void LicenseFamiliesTest() throws URISyntaxException { XMLConfigurationReader reader = new XMLConfigurationReader(); URL url = XMLConfigurationReaderTest.class.getResource("/org/apache/rat/default.xml"); + assertThat(url).isNotNull(); reader.read(url.toURI()); - assertThat(reader.readFamilies().stream().map(x -> x.getFamilyCategory().trim()).toArray(String[]::new)) - .containsExactly(EXPECTED_IDS); + Collection actual = reader.readFamilies().stream().map(lf -> lf.getFamilyCategory().trim()) + .toList(); + assertThat(actual).containsExactlyInAnyOrder(EXPECTED_IDS); } private void checkMatcher(String name, Class extends AbstractBuilder> clazz) { diff --git a/apache-rat-core/src/test/java/org/apache/rat/report/claim/ClaimStatisticTest.java b/apache-rat-core/src/test/java/org/apache/rat/report/claim/ClaimStatisticTest.java new file mode 100644 index 000000000..b44f0e189 --- /dev/null +++ b/apache-rat-core/src/test/java/org/apache/rat/report/claim/ClaimStatisticTest.java @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * https://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + */ +package org.apache.rat.report.claim; + +import org.apache.rat.api.Document; +import org.junit.jupiter.api.Test; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ClaimStatisticTest { + + @Test + void counterTests() { + ClaimStatistic underTest = new ClaimStatistic(); + for (ClaimStatistic.Counter counter : ClaimStatistic.Counter.values()) { + assertThat(underTest.getCounter(counter)).isZero(); + underTest.incCounter(counter, 1); + assertThat(underTest.getCounter(counter)).isEqualTo(1); + underTest.incCounter(counter, -2); + assertThat(underTest.getCounter(counter)).isEqualTo(-1); + underTest.setCounter(counter, Integer.MAX_VALUE); + assertThat(underTest.getCounter(counter)).isEqualTo(Integer.MAX_VALUE); + } + } + + @Test + void typeTests() { + ClaimStatistic underTest = new ClaimStatistic(); + for (Document.Type docType : Document.Type.values()) { + assertThat(underTest.getCounter(docType)).isZero(); + underTest.incCounter(docType, 1); + assertThat(underTest.getCounter(docType)).isEqualTo(1); + underTest.incCounter(docType, -2); + assertThat(underTest.getCounter(docType)).isEqualTo(-1); + } + } + + @Test + void documentTypeTest() { + ClaimStatistic underTest = new ClaimStatistic(); + assertThat(underTest.getDocumentTypes()).isEmpty(); + underTest.incCounter(Document.Type.NOTICE, 1); + underTest.incCounter(Document.Type.BINARY, -1); + assertThat(underTest.getDocumentTypes()).containsExactly(Document.Type.BINARY, Document.Type.NOTICE); + } + + @Test + void licenseCategoryCountTest() { + ClaimStatistic underTest = new ClaimStatistic(); + assertThat(underTest.getLicenseCategoryCount("fam")).isZero(); + underTest.incLicenseCategoryCount("fam", 1); + assertThat(underTest.getLicenseCategoryCount("fam")).isEqualTo(1); + underTest.incLicenseCategoryCount("fam", -2); + assertThat(underTest.getLicenseCategoryCount("fam")).isEqualTo(-1); + underTest.incLicenseCategoryCount("wayToLongAName", 5); + assertThat(underTest.getLicenseCategoryCount("wayToLongAName")).isEqualTo(5); + } + + @Test + void licenseNameCountTest() { + ClaimStatistic underTest = new ClaimStatistic(); + assertThat(underTest.getLicenseNameCount("fam")).isZero(); + underTest.incLicenseNameCount("fam", 1); + assertThat(underTest.getLicenseNameCount("fam")).isEqualTo(1); + underTest.incLicenseNameCount("fam", -2); + assertThat(underTest.getLicenseNameCount("fam")).isEqualTo(-1); + } + + @Test + void licenseFamilyCategoriesTest() { + ClaimStatistic underTest = new ClaimStatistic(); + assertThat(underTest.getLicenseFamilyCategories()).isEmpty(); + underTest.incLicenseCategoryCount("one", 1); + underTest.incLicenseCategoryCount("neg", -1); + assertThat(underTest.getLicenseFamilyCategories()).containsExactly("neg", "one"); + underTest.incLicenseCategoryCount("wayToLongAName", 5); + assertThat(underTest.getLicenseFamilyCategories()).containsExactly("neg", "one", "wayToLongAName"); + } + + @Test + void licenseFamilyNameTest() { + ClaimStatistic underTest = new ClaimStatistic(); + assertThat(underTest.getLicenseNames()).isEmpty(); + underTest.incLicenseNameCount("one", 1); + underTest.incLicenseNameCount("neg", -1); + assertThat(underTest.getLicenseNames()).containsExactly("neg", "one"); + underTest.incLicenseNameCount("wayToLongAName", 5); + assertThat(underTest.getLicenseNames()).containsExactly("neg", "one", "wayToLongAName"); + } + + /** + * Compares two claim statistics for similarity after serialization/deserialization. + * @param actual the deserialized verison. + * @param expected the original version. + */ + public static void assertSame(final ClaimStatistic actual, final ClaimStatistic expected) { + + assertThat(actual.getLicenseFamilyCategories()).containsExactlyElementsOf(expected.getLicenseFamilyCategories()); + assertThat(actual.getLicenseNames()).containsExactlyElementsOf(expected.getLicenseNames()); + assertThat(actual.getDocumentTypes()).containsExactlyElementsOf(expected.getDocumentTypes()); + for (String cat : expected.getLicenseFamilyCategories()) { + assertThat(actual.getLicenseCategoryCount(cat)).isEqualTo(expected.getLicenseCategoryCount(cat)); + } + for (String name : expected.getLicenseNames()) { + assertThat(actual.getLicenseNameCount(name)).isEqualTo(expected.getLicenseNameCount(name)); + } + for (Document.Type type : expected.getDocumentTypes()) { + assertThat(actual.getCounter(type)).isEqualTo(expected.getCounter(type)); + } + for (ClaimStatistic.Counter counter : ClaimStatistic.Counter.values()) { + assertThat(actual.getCounter(counter)).isEqualTo(expected.getCounter(counter)); + } + } + + @Test + void serdeRoundTrip() throws IOException { + ClaimStatistic underTest = new ClaimStatistic(); + underTest.incLicenseCategoryCount("familyCagegory", 1); + underTest.incCounter(ClaimStatistic.Counter.APPROVED, 2); + underTest.incCounter(Document.Type.IGNORED, 3); + underTest.incLicenseNameCount("licenseName", 4); + + ClaimStatistic.Serde serde = underTest.serde(); + StringWriter stringWriter = new StringWriter(); + serde.serialize(stringWriter); + ClaimStatistic actual = new ClaimStatistic(); + ClaimStatistic.Serde serde2 = actual.serde(); + serde2.deserialize(() -> new ByteArrayInputStream(stringWriter.toString().getBytes(StandardCharsets.UTF_8))); + + assertSame(actual, underTest); + } +} diff --git a/apache-rat-core/src/test/java/org/apache/rat/report/xml/writer/impl/base/XmlWriterTest.java b/apache-rat-core/src/test/java/org/apache/rat/report/xml/writer/XmlWriterTest.java similarity index 98% rename from apache-rat-core/src/test/java/org/apache/rat/report/xml/writer/impl/base/XmlWriterTest.java rename to apache-rat-core/src/test/java/org/apache/rat/report/xml/writer/XmlWriterTest.java index 287db7677..b86ad72f0 100644 --- a/apache-rat-core/src/test/java/org/apache/rat/report/xml/writer/impl/base/XmlWriterTest.java +++ b/apache-rat-core/src/test/java/org/apache/rat/report/xml/writer/XmlWriterTest.java @@ -16,11 +16,8 @@ * specific language governing permissions and limitations * * under the License. * */ -package org.apache.rat.report.xml.writer.impl.base; +package org.apache.rat.report.xml.writer; -import org.apache.rat.report.xml.writer.InvalidXmlException; -import org.apache.rat.report.xml.writer.OperationNotAllowedException; -import org.apache.rat.report.xml.writer.XmlWriter; import org.apache.rat.testhelpers.XmlUtils; import org.junit.jupiter.api.Test; import org.w3c.dom.Document; diff --git a/apache-rat-core/src/test/java/org/apache/rat/test/AbstractConfigurationOptionsProvider.java b/apache-rat-core/src/test/java/org/apache/rat/test/AbstractConfigurationOptionsProvider.java index 84e30dc59..5bee29740 100644 --- a/apache-rat-core/src/test/java/org/apache/rat/test/AbstractConfigurationOptionsProvider.java +++ b/apache-rat-core/src/test/java/org/apache/rat/test/AbstractConfigurationOptionsProvider.java @@ -19,6 +19,7 @@ package org.apache.rat.test; import java.io.FileWriter; +import java.nio.charset.StandardCharsets; import java.nio.file.FileSystems; import java.nio.file.Path; import org.apache.commons.cli.Option; @@ -80,7 +81,7 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio */ public static void preserveData(File baseDir, String targetDir) { final Path recordPath = FileSystems.getDefault().getPath("target", targetDir); - org.apache.rat.testhelpers.FileUtils.mkDir(recordPath.toFile()); + org.apache.rat.utils.FileUtils.mkDir(recordPath.toFile()); try { FileUtils.copyDirectory(baseDir, recordPath.toFile()); } catch (IOException e) { @@ -232,10 +233,10 @@ protected void inputExcludeParsedScmTest() { writeFile(".gitignore", Arrays.asList(lines)); File dir = new File(baseDir, "red"); - org.apache.rat.testhelpers.FileUtils.mkDir(dir); + org.apache.rat.utils.FileUtils.mkDir(dir); dir = new File(baseDir, "blue"); dir = new File(dir, "fish"); - org.apache.rat.testhelpers.FileUtils.mkDir(dir); + org.apache.rat.utils.FileUtils.mkDir(dir); assertDoesNotThrow(() -> { ReportConfiguration config = generateConfig(ImmutablePair.of(option, args)); @@ -815,12 +816,19 @@ private void styleSheetTest(final Option option) { // run the test String[] args = {null}; assertDoesNotThrow(() -> { - for (String sheet : new String[]{"plain-rat", "missing-headers", "unapproved-licenses", file.getAbsolutePath()}) { + for (String sheet : new String[]{"plain-rat", "missing-headers", "unapproved-licenses", "stylesheet-" + option.getLongOpt()}) { args[0] = sheet; ReportConfiguration config = generateConfig(ImmutablePair.of(option, args)); - try (InputStream expected = StyleSheets.getStyleSheet(sheet).ioSupplier().get(); + DocumentName base = DocumentName.builder(baseDir).build(); + DocumentName expectedName = base.resolve(sheet); + try (InputStream expected = StyleSheets.getStyleSheet(sheet, base).ioSupplier().get(); InputStream actual = config.getStyleSheet().get()) { - assertThat(IOUtils.contentEquals(expected, actual)).as(() -> String.format("'%s' does not match", sheet)).isTrue(); + String expectedStr = IOUtils.toString(expected, StandardCharsets.UTF_8); + String actualStr = IOUtils.toString(actual, StandardCharsets.UTF_8); + assertThat(actualStr).as(() -> String.format("'%s' does not match '%s': %s != %s", + config.getStyleSheetDescriptor().name(), + expectedName.getName(), + actualStr, expectedStr)).isEqualTo(expectedStr); } } }); @@ -845,7 +853,7 @@ protected void scanHiddenDirectoriesTest() { protected void xmlTest() { assertDoesNotThrow(() -> { ReportConfiguration config = generateConfig(ImmutablePair.of(Arg.OUTPUT_STYLE.find("xml"), null)); - try (InputStream expected = StyleSheets.getStyleSheet("xml").ioSupplier().get(); + try (InputStream expected = StyleSheets.getStyleSheet("xml", null).ioSupplier().get(); InputStream actual = config.getStyleSheet().get()) { assertThat(IOUtils.contentEquals(expected, actual)).as("'xml' does not match").isTrue(); } diff --git a/apache-rat-core/src/test/java/org/apache/rat/test/AbstractOptionsProvider.java b/apache-rat-core/src/test/java/org/apache/rat/test/AbstractOptionsProvider.java index f4a188099..1f55d2ba0 100644 --- a/apache-rat-core/src/test/java/org/apache/rat/test/AbstractOptionsProvider.java +++ b/apache-rat-core/src/test/java/org/apache/rat/test/AbstractOptionsProvider.java @@ -170,7 +170,7 @@ public static String[] extractArgs(List> args) { } protected File writeFile(final String name, final Iterable lines) { - return org.apache.rat.testhelpers.FileUtils.writeFile(baseDir, name, lines); + return org.apache.rat.utils.FileUtils.writeFile(baseDir, name, lines); } final protected DocumentName mkDocName(final String name) { diff --git a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/BaseOption.java b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/BaseOption.java new file mode 100644 index 000000000..4a22d9d7e --- /dev/null +++ b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/BaseOption.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * https://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + */ +package org.apache.rat.testhelpers; + +import org.apache.commons.cli.Option; +import org.apache.rat.ui.ArgumentTracker; +import org.apache.rat.ui.UIOption; +import org.apache.rat.ui.UIOptionCollection; +import org.apache.rat.utils.CasedString; + +public final class BaseOption extends UIOption { + BaseOption(final UIOptionCollection collection, Option option) { + super(collection, option, new CasedString(CasedString.StringCase.KEBAB, ArgumentTracker.extractKey(option))); + } + protected String cleanupName(Option option) { + return ArgumentTracker.extractKey(option); + } + + public String getExample() { + return ""; + } + + public String getText() { + return ""; + } +} diff --git a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/BaseOptionCollection.java b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/BaseOptionCollection.java new file mode 100644 index 000000000..3500c44d0 --- /dev/null +++ b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/BaseOptionCollection.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * https://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + */ +package org.apache.rat.testhelpers; + +import org.apache.rat.ui.UIOptionCollection; + +public final class BaseOptionCollection extends UIOptionCollection { + private BaseOptionCollection(Builder builder) { + super(builder); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends UIOptionCollection.Builder { + private Builder() { + super(BaseOption::new); + } + + @Override + public BaseOptionCollection build() { + return new BaseOptionCollection(this); + } + } +} diff --git a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/TestingLog.java b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/TestingLog.java index 171cacacd..0298eb52c 100644 --- a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/TestingLog.java +++ b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/TestingLog.java @@ -26,6 +26,7 @@ public class TestingLog implements Log { private StringBuilder captured = new StringBuilder(); + private Log.Level level = Log.Level.INFO; /** * Clears the captured buffer @@ -86,12 +87,18 @@ public void assertNotContainsPattern(String pattern) { @Override public Level getLevel() { - return Level.DEBUG; + return level; + } + + @Override + public void setLevel(Level level) { + this.level = level; } @Override public void log(Level level, String msg) { - captured.append(String.format("%s: %s%n", level, msg)); + if (isEnabled(level)) + captured.append(String.format("%s: %s%n", level, msg)); } /** diff --git a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/TextUtils.java b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/TextUtils.java index e90a30b9c..72931b49e 100644 --- a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/TextUtils.java +++ b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/TextUtils.java @@ -40,7 +40,9 @@ public class TextUtils { * * @param pattern the pattern to match. * @param target the string to match. + * @deprecated use assertThat(target).containsPattern(pattern) */ + @Deprecated public static void assertPatternInTarget(String pattern, String target) { assertThat(isMatching(pattern, target)).as(() -> format("Target does not match string: %s%n%s", pattern, target)) .isTrue(); @@ -51,7 +53,9 @@ public static void assertPatternInTarget(String pattern, String target) { * * @param pattern the pattern to match. * @param target the string to match. + * @deprecated use assertThat(target).dosNotContainPattern(pattern) */ + @Deprecated public static void assertPatternNotInTarget(String pattern, String target) { assertThat(isMatching(pattern, target)).as(() -> format("Target matches the pattern: %s%n%s", pattern, target)) .isFalse(); @@ -63,7 +67,9 @@ public static void assertPatternNotInTarget(String pattern, String target) { * @param pattern the pattern to match. * @param target the string to match. * @return {@code true} if a regular expression pattern is in a string + * @deprecated use assertThat(target).matches(pattern) */ + @Deprecated public static boolean isMatching(final String pattern, final String target) { return Pattern.compile(pattern, Pattern.MULTILINE).matcher(target).find(); } diff --git a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/AbstractTestDataProvider.java b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/AbstractTestDataProvider.java new file mode 100644 index 000000000..d72ea19b5 --- /dev/null +++ b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/AbstractTestDataProvider.java @@ -0,0 +1,201 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + */ +package org.apache.rat.testhelpers.data; + + +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Stream; +import org.apache.commons.cli.Option; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.rat.OptionCollectionParser; +import org.apache.rat.commandline.Arg; +import org.apache.rat.ui.UIOptionCollection; +import org.apache.rat.ui.ArgumentTracker; +import org.apache.rat.utils.DefaultLog; + +/** + * Generates a list of TestData for executing the Report. + * Use of this interface ensures consistent testing across the UIs. Each method + * tests an Option from {@link OptionCollectionParser} that must be implemented in the UI. + */ +public abstract class AbstractTestDataProvider { + + /** The list of exclude args */ + static final String[] EXCLUDE_ARGS = {"*.foo", "%regex[[A-Z]\\.bar]", "justbaz"}; + /** the list of include args */ + static final String[] INCLUDE_ARGS = {"B.bar", "justbaz"}; + // Sonar suggests List of but we need an Immutable list. + public static final ImmutableList> NO_OPTIONS = ImmutableList.of(ImmutablePair.nullPair()); // NOSONAR + + /** + * Generates a map of TestData indexed by the testName + * @param optionCollection the collection of options for the UI under test. + * @return the map of testName to Test Data. + */ + public final Map getOptionTestMap(final UIOptionCollection> optionCollection) { + Map map = new TreeMap<>(); + for (TestData test : getOptionTests(optionCollection)) { + map.put(test.getTestName(), test); + } + return map; + } + + /** + * Generates a list of test data for Option testing. + * This is different from UI testing as this is to test + * that the command line is properly parsed into a configuration. + * @param optionCollection the collection of options for the UI under test. + * @return a set of TestData for the tests. + */ + public final Set getOptionTests(final UIOptionCollection> optionCollection) { + // the optionCollection establishes any changes to the Arg values. + List unsupportedOptions = new ArrayList<>(optionCollection.getUnsupportedOptions().getOptions()); + Set result = new TreeSet<>(); + for (Arg arg : Arg.values()) { + if (!arg.isEmpty()) { + Stream options = arg.group().getOptions().stream().filter(opt -> !unsupportedOptions.contains(opt)); + switch (arg) { + case CONFIGURATION -> options.forEach(opt -> configTest(result, opt)); + case CONFIGURATION_NO_DEFAULTS -> options.forEach(opt -> configurationNoDefaultsTest(result, opt)); + case COUNTER_MIN -> options.forEach(opt -> counterMinTest(result, opt)); + case COUNTER_MAX -> options.forEach(opt -> counterMaxTest(result, opt)); + case DRY_RUN -> options.forEach(opt -> dryRunTest(result, opt)); + case EDIT_COPYRIGHT -> options.forEach(opt -> editCopyrightTest(result, opt)); + case EDIT_ADD -> options.forEach(opt -> editLicenseTest(result, opt)); + case EDIT_OVERWRITE -> options.forEach(opt -> editOverwriteTest(result, opt)); + case HELP_LICENSES -> options.forEach(opt -> helpLicenses(result, opt)); + case EXCLUDE -> options.forEach(opt -> inputExcludeTest(result, opt)); + case EXCLUDE_FILE -> options.forEach(opt -> inputExcludeFileTest(result, opt)); + case EXCLUDE_PARSE_SCM -> options.forEach(opt -> inputExcludeParsedScmTest(result, opt)); + case EXCLUDE_STD -> options.forEach(opt -> inputExcludeStdTest(result, opt)); + case EXCLUDE_SIZE -> options.forEach(opt -> inputExcludeSizeTest(result, opt)); + case INCLUDE -> options.forEach(opt -> inputIncludeTest(result, opt)); + case INCLUDE_FILE -> options.forEach(opt -> inputIncludeFileTest(result, opt)); + case INCLUDE_STD -> options.forEach(opt -> inputIncludeStdTest(result, opt)); + case SOURCE -> options.forEach(opt -> inputSourceTest(result, opt)); + case FAMILIES_APPROVED -> options.forEach(opt -> licenseFamiliesApprovedTest(result, opt)); + case FAMILIES_APPROVED_FILE -> options.forEach(opt -> licenseFamiliesApprovedFileTest(result, opt)); + case FAMILIES_DENIED -> options.forEach(opt -> licenseFamiliesDeniedTest(result, opt)); + case FAMILIES_DENIED_FILE -> options.forEach(opt -> licenseFamiliesDeniedFileTest(result, opt)); + case LICENSES_APPROVED -> options.forEach(opt -> licensesApprovedTest(result, opt)); + case LICENSES_APPROVED_FILE -> options.forEach(opt -> licensesApprovedFileTest(result, opt)); + case LICENSES_DENIED -> options.forEach(opt -> licensesDeniedTest(result, opt)); + case LICENSES_DENIED_FILE -> options.forEach(opt -> licensesDeniedFileTest(result, opt)); + case LOG_LEVEL -> options.forEach(opt -> logLevelTest(result, opt)); + case OUTPUT_ARCHIVE -> options.forEach(opt -> outputArchiveTest(result, opt)); + case OUTPUT_FAMILIES -> options.forEach(opt -> outputFamiliesTest(result, opt)); + case OUTPUT_FILE -> options.forEach(opt -> outputFileTest(result, opt)); + case OUTPUT_LICENSES -> options.forEach(opt -> outputLicensesTest(result, opt)); + case OUTPUT_STANDARD -> options.forEach(opt -> outputStandardTest(result, opt)); + case OUTPUT_STYLE -> options.forEach(opt -> outputStyleTest(result, opt)); + case DIR -> {/* do nothing */} + default -> throw new IllegalStateException("Unrecognized Arg: " + arg.name()); + } + } + } + validate(result); + return result; + } + + private void validate(Set result) { + Set options = new HashSet<>(Arg.getOptions().getOptions()); + result.forEach(testData -> options.remove(testData.getOption())); + // TODO fix this once deprecated options are removed NOSONAR + options.forEach(opt -> DefaultLog.getInstance().warn("Option " + ArgumentTracker.extractKey(opt) + " was not tested.")); + //assertThat(options).describedAs("All options are not accounted for.").isEmpty(); // NOSONAR + } + + protected abstract void inputExcludeFileTest(final Set result, final Option option); + + protected abstract void inputExcludeTest(final Set result, final Option option); + + protected abstract void inputExcludeStdTest(final Set result, final Option option); + + protected abstract void inputExcludeParsedScmTest(final Set result, final Option option); + + protected abstract void inputExcludeSizeTest(final Set result, final Option option); + + protected abstract void inputIncludeFileTest(final Set result, final Option option); + + protected abstract void inputIncludeTest(final Set result, final Option option); + + protected abstract void inputIncludeStdTest(final Set result, final Option option); + + protected abstract void inputSourceTest(final Set result, final Option option); + + protected abstract void helpLicenses(final Set result, final Option option); + + protected abstract void licensesApprovedFileTest(final Set result, final Option option); + + protected abstract void licensesApprovedTest(final Set result, final Option option); + + protected abstract void licensesDeniedTest(final Set result, final Option option); + + protected abstract void licensesDeniedFileTest(final Set result, final Option option); + + protected abstract void licenseFamiliesApprovedFileTest(final Set result, final Option option); + + protected abstract void licenseFamiliesApprovedTest(final Set result, final Option option); + + protected abstract void licenseFamiliesDeniedFileTest(final Set result, final Option option); + + protected abstract void licenseFamiliesDeniedTest(final Set result, final Option option); + + protected abstract void counterMaxTest(final Set result, final Option option); + + protected abstract void counterMinTest(final Set result, final Option option); + + /** + * Add results to the result list. + * @param result the result list. + * @param option configuration option we are testing. + */ + protected abstract void configTest(final Set result, final Option option); + + protected abstract void configurationNoDefaultsTest(final Set result, final Option option); + + protected abstract void dryRunTest(final Set result, final Option option); + + protected abstract void editCopyrightTest(final Set result, final Option option); + + protected abstract void editLicenseTest(final Set result, final Option option); + + protected abstract void editOverwriteTest(final Set result, final Option option); + + protected abstract void logLevelTest(final Set result, final Option option); + + protected abstract void outputArchiveTest(final Set result, final Option option); + + protected abstract void outputFamiliesTest(final Set result, final Option option); + + protected abstract void outputFileTest(final Set result, final Option option); + + protected abstract void outputLicensesTest(final Set result, final Option option); + + protected abstract void outputStandardTest(final Set result, final Option option); + + protected abstract void outputStyleTest(final Set result, final Option option); +} diff --git a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/DataUtils.java b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/DataUtils.java new file mode 100644 index 000000000..3b570161d --- /dev/null +++ b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/DataUtils.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + */ +package org.apache.rat.testhelpers.data; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; +import java.util.function.Consumer; +import org.apache.commons.cli.Option; +import org.apache.rat.report.xml.writer.XmlWriter; +import org.apache.rat.utils.CasedString; +import org.apache.rat.utils.DefaultLog; + +public class DataUtils { + /** + * The text for the current ASF license. + */ + public static final String[] ASF_TEXT_LINES = { + "Licensed to the Apache Software Foundation (ASF) under one or more", + "contributor license agreements. See the NOTICE file distributed with", + "this work for additional information regarding copyright ownership.", + "The ASF licenses this file to You under the Apache License, Version 2.0", + "(the \"License\"); you may not use this file except in compliance with", + "the License. You may obtain a copy of the License at", + " ", + " https://www.apache.org/licenses/LICENSE-2.0", + " ", + "Unless required by applicable law or agreed to in writing, software", + "distributed under the License is distributed on an \"AS IS\" BASIS,", + "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.", + "See the License for the specific language governing permissions and", + "limitations under the License." + }; + + /** + * The text for the current ASF license as a block of text with new lines. + */ + public static final String ASF_TEXT = String.join("\n" ,ASF_TEXT_LINES); + + /** + * A setup that does nothing. + */ + public static final Consumer NO_SETUP = basePath -> { + DefaultLog.getInstance().warn("no setup for " + basePath); + }; + + public static final Consumer NO_VALIDATOR = validatorData -> {}; + + private DataUtils() { + // do not instantiate + } + + /** + * Create a directory name from the option. Directory names are camel. + * @param option the option to create a directory name from. + * @return the directory name for the option. + */ + static String asDirName(Option option) { + if (option.hasLongOpt()) { + return CasedString.StringCase.CAMEL.assemble(CasedString.StringCase.KEBAB.getSegments(option.getLongOpt())); + } + return option.getOpt().toLowerCase(Locale.ROOT); + } + + /** + * Generate a simple configuration file that defines one license based on the name of the file and + * containing as single text matcher that mateches the {@code text} parameter. + * @param fileName The name of the file to create. + * @param id the ID for the family. + * @param text the text for the matcher. + */ + static void generateTextConfig(Path fileName, String id, String text) { + try (XmlWriter writer = new XmlWriter(new OutputStreamWriter(Files.newOutputStream(fileName.toFile().toPath()), + StandardCharsets.UTF_8))) { + writer.startDocument() + .comment(ASF_TEXT) + .startElement("rat-config") + .startElement("families") + .startElement("family") + .attribute("id", id) + .attribute("name", "from " + fileName.getFileName()) + .closeElement() // family + .closeElement() // families + .startElement("licenses") + .startElement("license") + .attribute("family", id) + .startElement("text") + .content(text) + .closeDocument(); // close all open elements + } catch (IOException e) { + throw new RuntimeException(e); + } + } + /** + * Generate a simple configuration file that defines one license based on the name of the file and + * containing as single text matcher that mateches the {@code text} parameter. + * @param fileName The name of the file to create. + * @param id the ID for the family. + * @param spdxId the SPDX id for the matcher. + */ + static void generateSpdxConfig(Path fileName, String id, String spdxId) { + try (XmlWriter writer = new XmlWriter(new OutputStreamWriter(Files.newOutputStream(fileName.toFile().toPath()), + StandardCharsets.UTF_8))) { + writer.startDocument() + .comment(ASF_TEXT) + .startElement("rat-config") + .startElement("families") + .startElement("family") + .attribute("id", id) + .attribute("name", "from " + fileName.getFileName()) + .closeElement() // family + .closeElement() // families + .startElement("licenses") + .startElement("license") + .attribute("family", id) + .startElement("spdx") + .attribute("name", spdxId) + .closeDocument(); // close all open elements + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/ReportTestDataProvider.java b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/ReportTestDataProvider.java new file mode 100644 index 000000000..2f4375791 --- /dev/null +++ b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/ReportTestDataProvider.java @@ -0,0 +1,1282 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * https://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + */ +package org.apache.rat.testhelpers.data; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import org.apache.commons.cli.Option; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.rat.OptionCollectionParser; +import org.apache.rat.ReportConfiguration; +import org.apache.rat.api.RatException; +import org.apache.rat.commandline.Arg; +import org.apache.rat.commandline.StyleSheets; +import org.apache.rat.config.exclusion.StandardCollection; +import org.apache.rat.config.results.ClaimValidator; +import org.apache.rat.document.DocumentName; +import org.apache.rat.license.LicenseSetFactory; +import org.apache.rat.report.claim.ClaimStatistic; +import org.apache.rat.report.xml.writer.XmlWriter; +import org.apache.rat.utils.FileUtils; +import org.apache.rat.testhelpers.TestingLog; +import org.apache.rat.testhelpers.TextUtils; +import org.apache.rat.testhelpers.XmlUtils; +import org.apache.rat.utils.DefaultLog; +import org.apache.rat.utils.Log; +import org.w3c.dom.Document; + +import static org.apache.rat.utils.FileUtils.writeFile; +import static org.apache.rat.testhelpers.data.DataUtils.NO_SETUP; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Fail.fail; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +/** + * Generates a list of TestData for executing the Report. + * Use of this interface ensures consistent testing across the UIs. Each method + * tests an Option from {@link OptionCollectionParser} that must be implemented in the UI. + * These tests generally validate the results in the generated XML are as expected. + */ +public class ReportTestDataProvider extends AbstractTestDataProvider { + + public static final RatException NO_LICENSES_EXCEPTION = new RatException("At least one license must be defined"); + + private final XPath xpath = XPathFactory.newInstance().newXPath(); + + private final Consumer mkRat = basePath -> { + DefaultLog.getInstance().warn("mkRat setup for " + basePath); + File baseDir = basePath.toFile(); + File ratDir = new File(baseDir, ".rat"); + FileUtils.mkDir(ratDir); + }; + + private void assertStandardFile(Document document, String fname) { + try { + XmlUtils.assertIsPresent(document, xpath, + String.format("/rat-report/resource[@name='/%s'][@type='STANDARD']", fname)); + } catch (XPathExpressionException e) { + throw new RuntimeException(e); + } + } + + private void assertIgnoredFile(Document document, String fname) { + try { + XmlUtils.assertIsPresent(document, xpath, + String.format("/rat-report/resource[@name='/%s'][@type='IGNORED']", fname)); + } catch (XPathExpressionException e) { + throw new RuntimeException(e); + } + } + + private void assertCounter(ValidatorData data, ClaimStatistic.Counter counter, int count) { + assertThat(data.getStatistic().getCounter(counter)).as(counter.name()).isEqualTo(count); + } + + // exclude tests + private List execExcludeTest(final Option option, final Supplier args, Consumer setupFiles) { + Consumer setup = setupFiles.andThen(mkRat).andThen(basePath -> { + DefaultLog.getInstance().warn("execExcludeTest setup for " + basePath); + File baseDir = basePath.toFile(); + writeFile(baseDir, "notbaz"); + writeFile(baseDir, "well._afile"); + writeFile(baseDir, "some.foo"); + writeFile(baseDir, "B.bar"); + writeFile(baseDir, "justbaz"); + }); + + TestData test1 = new TestData(DataUtils.asDirName(option), NO_OPTIONS, + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating execExcludeTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 5); + assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 1); + }); + + String[] ignored = {"B.bar", "justbaz", "some.foo", ".rat"}; + String[] standard = {"well._afile", "notbaz"}; + TestData test2 = new TestData("", Collections.singletonList(ImmutablePair.of(option, args.get())), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating execExcludeTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, standard.length); + assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, ignored.length); + for (String fileName : ignored) { + assertIgnoredFile(validatorData.getDocument(), fileName); + } + for (String fileName : standard) { + assertStandardFile(validatorData.getDocument(), fileName); + } + }); + return Arrays.asList(test1, test2); + } + + protected void inputExcludeFileTest(final Set result, final Option option) { + Consumer setup = mkRat.andThen(baseDir -> { + File dir = baseDir.resolve(".rat").toFile(); + writeFile(dir, "exclude.txt", Arrays.asList(AbstractTestDataProvider.EXCLUDE_ARGS)); + }); + Supplier args = () -> new String[]{".rat/exclude.txt"}; + result.addAll(execExcludeTest(option, args, setup)); + } + + + protected void inputExcludeTest(final Set result, final Option option) { + result.addAll(execExcludeTest(option, () -> AbstractTestDataProvider.EXCLUDE_ARGS, x -> { + })); + } + + protected void inputExcludeStdTest(final Set result, final Option option) { + String[] args = {StandardCollection.MAVEN.name()}; + String[] defaultExcluded = {"afile~", ".#afile", "%afile%", "._afile"}; + String[] defaultIncluded = {"afile~more", "what.#afile", "%afile%withMore", "well._afile"}; + String mavenFile = "build.log"; + Consumer setup = basePath -> { + DefaultLog.getInstance().warn("inputExcludeStdTest setup for " + basePath); + File baseDir = basePath.toFile(); + for (String fileName : defaultExcluded) { + writeFile(baseDir, fileName); + } + for (String fileName : defaultIncluded) { + writeFile(baseDir, fileName); + } + writeFile(baseDir, mavenFile); + }; + result.add(new TestData(DataUtils.asDirName(option), Collections.singletonList(ImmutablePair.nullPair()), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating inputExcludeStdTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, defaultIncluded.length + 1); + assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, defaultExcluded.length); + for (String fileName : defaultIncluded) { + assertStandardFile(validatorData.getDocument(), fileName); + } + for (String fileName : defaultExcluded) { + assertIgnoredFile(validatorData.getDocument(), fileName); + } + })); + + + result.add(new TestData("", Collections.singletonList(ImmutablePair.of(option, args)), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating inputExcludeStdTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, defaultIncluded.length); + assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, defaultExcluded.length + 1); + for (String fileName : defaultIncluded) { + assertStandardFile(validatorData.getDocument(), fileName); + } + for (String fileName : defaultExcluded) { + assertIgnoredFile(validatorData.getDocument(), fileName); + } + assertIgnoredFile(validatorData.getDocument(), mavenFile); + })); + } + + protected void inputExcludeParsedScmTest(final Set result, final Option option) { + Consumer setup = basePath -> { + DefaultLog.getInstance().warn("inputExcludeParsedScmTest setup for " + basePath); + File baseDir = basePath.toFile(); + String[] lines = { + "# somethings", + "!thingone", "thing*", System.lineSeparator(), + "# some fish", + "**/fish", "*_fish", + "# some colorful directories", + "red/", "blue/*/"}; + writeFile(baseDir, ".gitignore", Arrays.asList(lines)); + writeFile(baseDir, "thingone"); + writeFile(baseDir, "thingtwo"); + File dir = new File(baseDir, "dir"); + FileUtils.mkDir(dir); + FileUtils.writeFile(dir, "fish_two"); + FileUtils.writeFile(dir, "fish"); + + dir = new File(baseDir, "red"); + FileUtils.mkDir(dir); + FileUtils.writeFile(dir, "fish"); + + dir = new File(baseDir, "blue/fish"); + FileUtils.mkDir(dir); + FileUtils.writeFile(dir, "dory"); + + dir = new File(baseDir, "some"); + FileUtils.mkDir(dir); + FileUtils.writeFile(dir, "fish"); + FileUtils.writeFile(dir, "things"); + FileUtils.writeFile(dir, "thingone"); + + dir = new File(baseDir, "another"); + FileUtils.mkDir(dir); + FileUtils.writeFile(dir, "red_fish"); + }; + + result.add(new TestData(DataUtils.asDirName(option), Collections.singletonList(ImmutablePair.nullPair()), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating inputExcludeParsedScmTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 11); + // .gitignore is ignored by default as it is hidden + assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 0); + })); + + result.add(new TestData("GIT", Collections.singletonList(ImmutablePair.of(option, new String[]{"GIT"})), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating inputExcludeParsedScmTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 3); + // .gitignore is ignored by default as it is hidden + assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 8); + })); + } + + protected void inputExcludeSizeTest(final Set result, final Option option) { + String[] notExcluded = {"Hello.txt", "HelloWorld.txt"}; + String[] excluded = {"Hi.txt"}; + + Consumer setup = basePath -> { + DefaultLog.getInstance().warn("inputExcludeSizeTest setup for " + basePath); + File baseDir = basePath.toFile(); + writeFile(baseDir, "Hi.txt", Collections.singletonList("Hi")); + writeFile(baseDir, "Hello.txt", Collections.singletonList("Hello")); + writeFile(baseDir, "HelloWorld.txt", Collections.singletonList("HelloWorld")); + }; + + result.add(new TestData(DataUtils.asDirName(option), Collections.singletonList(ImmutablePair.of(null, null)), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating inputExcludeSizeTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 3); + assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 0); + for (String fname : excluded) { + assertStandardFile(validatorData.getDocument(), fname); + } + for (String fname : notExcluded) { + assertStandardFile(validatorData.getDocument(), fname); + } + })); + + result.add(new TestData("", Collections.singletonList(ImmutablePair.of(option, new String[]{"5"})), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating inputExcludeSizeTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 2); + assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 1); + for (String fname : excluded) { + assertIgnoredFile(validatorData.getDocument(), fname); + } + for (String fname : notExcluded) { + assertStandardFile(validatorData.getDocument(), fname); + } + })); + } + + // include tests + private List execIncludeTest(final Option option, String[] args, Consumer setupFiles) { + Option excludeOption = Arg.EXCLUDE.option(); + Consumer setup = setupFiles.andThen(basePath -> { + DefaultLog.getInstance().warn("execIncludeTest setup for " + basePath); + File baseDir = basePath.toFile(); + writeFile(baseDir, "notbaz"); + writeFile(baseDir, "some.foo"); + writeFile(baseDir, "B.bar"); + writeFile(baseDir, "justbaz"); + }); + + // standard without options. + TestData test1 = new TestData(DataUtils.asDirName(option), NO_OPTIONS, + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating execIncludeTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 4); + assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 1); + }); + + // verify exclude removes the files + TestData test2 = new TestData(DataUtils.asDirName(option), Collections.singletonList(ImmutablePair.of(excludeOption, AbstractTestDataProvider.EXCLUDE_ARGS)), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating execIncludeTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 1); + // .gitignore is ignored by default as it is hidden but not counted + assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 4); + }); + + TestData test3 = new TestData("", Arrays.asList(ImmutablePair.of(option, args), ImmutablePair.of(excludeOption, AbstractTestDataProvider.EXCLUDE_ARGS)), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating execIncludeTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 3); + // .gitignore is ignored by default as it is hidden but not counted + assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 2); + }); + return Arrays.asList(test1, test2, test3); + } + + protected void inputIncludeFileTest(final Set result, final Option option) { + Consumer setup = mkRat.andThen(basePath -> { + DefaultLog.getInstance().warn("inputIncludeFileTest setup for " + basePath); + File dir = basePath.resolve(".rat").toFile(); + writeFile(dir, "include.txt", Arrays.asList(AbstractTestDataProvider.INCLUDE_ARGS)); + }); + result.addAll(execIncludeTest(option, new String[]{".rat/include.txt"}, setup)); + } + + + protected void inputIncludeTest(final Set result, final Option option) { + result.addAll(execIncludeTest(option, AbstractTestDataProvider.INCLUDE_ARGS, mkRat)); + } + + protected void inputIncludeStdTest(final Set result, final Option option) { + Consumer setup = basePath -> { + DefaultLog.getInstance().warn("inputIncludeStdTest setup for " + basePath); + File baseDir = basePath.toFile(); + writeFile(baseDir, "afile~more"); + writeFile(baseDir, "afile~"); + writeFile(baseDir, ".#afile"); + writeFile(baseDir, "%afile%"); + writeFile(baseDir, "._afile"); + writeFile(baseDir, "what.#afile"); + writeFile(baseDir, "%afile%withMore"); + writeFile(baseDir, "well._afile"); + writeFile(baseDir, ".hiddenFile", "The hidden file"); + File hiddenDir = new File(baseDir, ".hiddenDir"); + FileUtils.mkDir(hiddenDir); + writeFile(hiddenDir, "aFile", "File in hidden directory"); + }; + if (Arg.EXCLUDE.isEmpty()) { + throw new RuntimeException(String.format("Can not test %s if there are no exclude file options supported", option.getKey())); + } + ImmutablePair excludes = ImmutablePair.of(Arg.EXCLUDE.option(), + new String[]{"*~more", "*~"}); + + + result.add(new TestData("includeStdValidation", Collections.singletonList(excludes), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating inputIncludeStdTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 4); + assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 6); + })); + + if (!option.hasArg()) { + if (option.getKey().equals("scan-hidden-directories")) { + result.add(new TestData("", + Arrays.asList(ImmutablePair.of(option, null), excludes), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating inputIncludeStdTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 5); + assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 5); + assertIgnoredFile(validatorData.getDocument(), "._afile"); + assertStandardFile(validatorData.getDocument(), ".hiddenDir/aFile"); + })); + } else { + throw new RuntimeException("Unknown option: " + option.getKey()); + } + } else { + result.add(new TestData(StandardCollection.MISC.name().toLowerCase(Locale.ROOT), Arrays.asList(ImmutablePair.of(option, new String[]{StandardCollection.MISC.name()}), excludes), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating inputIncludeStdTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 8); + assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 2); + })); + + result.add(new TestData(StandardCollection.HIDDEN_FILE.name().toLowerCase(Locale.ROOT), + Arrays.asList(ImmutablePair.of(option, new String[]{StandardCollection.HIDDEN_FILE.name()}), excludes), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating inputIncludeStdTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 6); + assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 4); + assertStandardFile(validatorData.getDocument(), "._afile"); + assertIgnoredFile(validatorData.getDocument(), ".hiddenDir"); + })); + + result.add(new TestData(StandardCollection.HIDDEN_DIR.name().toLowerCase(Locale.ROOT), + Arrays.asList(ImmutablePair.of(option, new String[]{StandardCollection.HIDDEN_DIR.name()}), excludes), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating inputIncludeStdTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 5); + assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 5); + assertIgnoredFile(validatorData.getDocument(), "._afile"); + assertStandardFile(validatorData.getDocument(), ".hiddenDir/aFile"); + })); + } + } + + protected void inputSourceTest(final Set result, final Option option) { + Consumer setup = basePath -> { + DefaultLog.getInstance().warn("inputSourceTest setup for " + basePath); + File baseDir = basePath.toFile(); + writeFile(baseDir, "codefile", "https://www.apache.org/licenses/LICENSE-2.0"); + writeFile(baseDir, "input.txt", "codefile"); + writeFile(baseDir, "notcodeFile"); + }; + + result.add(new TestData(DataUtils.asDirName(option), NO_OPTIONS, + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating inputSourceTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 3); + assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 0); + })); + + result.add(new TestData("", Collections.singletonList(ImmutablePair.of(option, new String[]{"input.txt"})), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating inputSourceTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 1); + assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 0); + })); + } + + // LICENSE tests + protected List execLicensesApprovedTest(final Option option, String[] args, Consumer extraSetup) { + Consumer setup = extraSetup.andThen( + basePath -> { + DefaultLog.getInstance().warn("execLicensesApprovedTest setup for " + basePath); + File baseDir = basePath.toFile(); + writeFile(baseDir, "catz.txt", "SPDX-License-Identifier: catz"); + writeFile(baseDir, "apl.txt", "SPDX-License-Identifier: Apache-2.0"); + } + ); + + TestData test1 = new TestData(DataUtils.asDirName(option), NO_OPTIONS, + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating execLicensesApprovedTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 2); + assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 1); + assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 1); + }); + + TestData test2 = new TestData("withoutLicenseDef", Collections.singletonList(ImmutablePair.of(option, args)), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating execLicensesApprovedTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 2); + assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 1); + assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 1); + }); + + Option configOpt = Arg.CONFIGURATION.option(); + TestData test3 = new TestData("withLicenseDef", Arrays.asList(ImmutablePair.of(option, args), + ImmutablePair.of(configOpt, new String[]{".rat/catz.xml"})), + setup.andThen(mkRat).andThen( + basePath -> { + DefaultLog.getInstance().warn("withLicenseDef setup for " + basePath); + DataUtils.generateSpdxConfig(basePath.resolve(".rat").resolve("catz.xml"), "catz", "catz"); + }), + validatorData -> { + DefaultLog.getInstance().warn("validating execLicensesApprovedTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 2); + assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 2); + assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 0); + }); + return Arrays.asList(test1, test2, test3); + } + + protected void helpLicenses(final Set result, final Option option) { + PrintStream origin = System.out; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(baos); + + result.add(new TestData("stdOut", + Collections.singletonList(ImmutablePair.of(option, null)), + basePath -> { + DefaultLog.getInstance().warn("helpLicenses setup for " + basePath); + System.setOut(out); + }, + validatorData -> { + DefaultLog.getInstance().warn("validating helpLicenses for " + validatorData.getBaseDir()); + System.setOut(origin); + String txt = baos.toString(); + TextUtils.assertContains("====== Licenses ======", txt); + TextUtils.assertContains("====== Defined Matchers ======", txt); + TextUtils.assertContains("====== Defined Families ======", txt); + })); + } + + protected void licensesApprovedFileTest(final Set result, final Option option) { + result.addAll(execLicensesApprovedTest(option, new String[]{".rat/licensesApproved.txt"}, + mkRat.andThen( + basePath -> { + DefaultLog.getInstance().warn("licensesApprovedFileTest setup for " + basePath); + writeFile(basePath.resolve(".rat").toFile(), "licensesApproved.txt", List.of("catz")); + }))); + } + + protected void licensesApprovedTest(final Set result, final Option option) { + result.addAll(execLicensesApprovedTest(option, new String[]{"catz"}, NO_SETUP)); + } + + private List execLicensesDeniedTest(final Option option, final String[] args, Consumer setupFiles) { + Consumer setup = setupFiles.andThen(basePath -> { + DefaultLog.getInstance().warn("execLicensesDeniedTest setup for " + basePath); + File baseDir = basePath.toFile(); + writeFile(baseDir, "illumousFile.java", "The contents of this file are " + + "subject to the terms of the Common Development and Distribution License (the \"License\") You " + + "may not use this file except in compliance with the License."); + }); + TestData test1 = new TestData("ILLUMOS", Collections.singletonList(ImmutablePair.of(option, args)), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating execLicensesDeniedTest for " + validatorData.getBaseDir()); + ClaimStatistic claimStatistic = validatorData.getStatistic(); + ClaimValidator validator = validatorData.getConfiguration().getClaimValidator(); + assertThat(validator.listIssues(claimStatistic)).containsExactly("UNAPPROVED"); + }); + return Collections.singletonList(test1); + } + + protected void licensesDeniedTest(final Set result, final Option option) { + result.addAll(execLicensesDeniedTest(option, new String[]{"ILLUMOS"}, NO_SETUP)); + } + + protected void licensesDeniedFileTest(final Set result, final Option option) { + result.addAll(execLicensesDeniedTest(option, new String[]{"licensesDenied.txt"}, + basePath -> writeFile(basePath.toFile(), "licensesDenied.txt", Collections.singletonList("ILLUMOS")))); + } + + private List execLicenseFamiliesApprovedTest(final Option option, final String[] args, Consumer extraSetup) { + Consumer setup = extraSetup.andThen( + basePath -> { + DefaultLog.getInstance().warn("execLicenseFamiliesApprovedTest setup for " + basePath); + File baseDir = basePath.toFile(); + writeFile(baseDir, "catz.txt", "SPDX-License-Identifier: catz"); + }); + + TestData test1 = new TestData(DataUtils.asDirName(option), NO_OPTIONS, + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating execLicenseFamiliesApprovedTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 1); + assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 0); + assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 1); + }); + + TestData test2 = new TestData("withoutLicenseDef", Collections.singletonList(ImmutablePair.of(option, args)), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating execLicenseFamiliesApprovedTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 1); + assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 0); + assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 1); + }); + Option configOpt = Arg.CONFIGURATION.option(); + TestData test3 = new TestData("withLicenseDef", Arrays.asList(ImmutablePair.of(option, args), + ImmutablePair.of(configOpt, new String[]{".rat/catz.xml"})), + setup.andThen(mkRat).andThen( + basePath -> { + DefaultLog.getInstance().warn("withLicenseDef setup for " + basePath); + Path ratDir = basePath.resolve(".rat"); + Path catzXml = ratDir.resolve("catz.xml"); + DataUtils.generateSpdxConfig(catzXml, "catz", "catz"); + }), + validatorData -> { + DefaultLog.getInstance().warn("validating execLicenseFamiliesApprovedTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 1); + assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 1); + assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 0); + }); + return Arrays.asList(test1, test2, test3); + } + + protected void licenseFamiliesApprovedFileTest(final Set result, final Option option) { + result.addAll(execLicenseFamiliesApprovedTest(option, new String[]{".rat/familiesApproved.txt"}, + mkRat.andThen(basePath -> { + DefaultLog.getInstance().warn("licenseFamiliesApprovedFileTest setup for " + basePath); + writeFile(basePath.resolve(".rat").toFile(), "familiesApproved.txt", Collections.singletonList("catz")); + }))); + } + + protected void licenseFamiliesApprovedTest(final Set result, final Option option) { + result.addAll(execLicenseFamiliesApprovedTest(option, new String[]{"catz"}, NO_SETUP)); + } + + private List execLicenseFamiliesDeniedTest(final Option option, final String[] args, Consumer extraSetup) { + Consumer setup = extraSetup.andThen( + basePath -> { + DefaultLog.getInstance().warn("execLicenseFamiliesDeniedTest setup for " + basePath); + File baseDir = basePath.toFile(); + writeFile(baseDir, "bsd.txt", "SPDX-License-Identifier: BSD-3-Clause"); + }); + + TestData test1 = new TestData(DataUtils.asDirName(option), NO_OPTIONS, + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating execLicenseFamiliesDeniedTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 1); + assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 1); + assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 0); + }); + + TestData test2 = new TestData("", Collections.singletonList(ImmutablePair.of(option, args)), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating execLicenseFamiliesDeniedTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 1); + assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 0); + assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 1); + }); + + return Arrays.asList(test1, test2); + } + + protected void licenseFamiliesDeniedFileTest(final Set result, final Option option) { + result.addAll(execLicenseFamiliesDeniedTest(option, new String[]{".rat/familiesDenied.txt"}, + mkRat.andThen( + basePath -> { + DefaultLog.getInstance().warn("licenseFamiliesDeniedFileTest setup for " + basePath); + writeFile(basePath.resolve(".rat").toFile(), "familiesDenied.txt", Collections.singletonList("BSD-3")); + }))); + } + + protected void licenseFamiliesDeniedTest(final Set result, final Option option) { + result.addAll(execLicenseFamiliesDeniedTest(option, new String[]{"BSD-3"}, NO_SETUP)); + } + + protected void counterMaxTest(final Set result, final Option option) { + result.add(new TestData(DataUtils.asDirName(option), Collections.singletonList(ImmutablePair.of(null, null)), + basePath -> { + DefaultLog.getInstance().warn("counterMaxTest setup for " + basePath); + File baseDir = basePath.toFile(); + writeFile(baseDir, "Test.java", Arrays.asList("/*\n", "SPDX-License-Identifier: Unapproved\n", + "*/\n\n", "class Test {}\n")); + }, + validatorData -> { + DefaultLog.getInstance().warn("validating counterMaxTest for " + validatorData.getBaseDir()); + ClaimStatistic claimStatistic = validatorData.getStatistic(); + ClaimValidator validator = validatorData.getConfiguration().getClaimValidator(); + assertThat(validator.listIssues(claimStatistic)).containsExactly("UNAPPROVED"); + })); + + result.add(new TestData("Unapproved1", Collections.singletonList(ImmutablePair.of(option, new String[]{"Unapproved:1"})), + basePath -> { + DefaultLog.getInstance().warn("counterMaxTest setup for " + basePath); + File baseDir = basePath.toFile(); + writeFile(baseDir, "Test.java", Arrays.asList("/*\n", "SPDX-License-Identifier: Unapproved\n", + "*/\n\n", "class Test {}\n")); + }, + validatorData -> { + DefaultLog.getInstance().warn("validating counterMaxTest for " + validatorData.getBaseDir()); + ClaimStatistic claimStatistic = validatorData.getStatistic(); + ClaimValidator validator = validatorData.getConfiguration().getClaimValidator(); + assertThat(validator.listIssues(claimStatistic)).isEmpty(); + })); + } + + protected void counterMinTest(final Set result, final Option option) { + result.add(new TestData(DataUtils.asDirName(option), NO_OPTIONS, + basePath -> { + DefaultLog.getInstance().warn("counterMinTest setup for " + basePath); + File baseDir = basePath.toFile(); + writeFile(baseDir, "Test.java", Arrays.asList("/*\n", "SPDX-License-Identifier: Unapproved\n", + "*/\n\n", "class Test {}\n")); + }, + validatorData -> { + DefaultLog.getInstance().warn("validating counterMinTest for " + validatorData.getBaseDir()); + ClaimStatistic claimStatistic = validatorData.getStatistic(); + ClaimValidator validator = validatorData.getConfiguration().getClaimValidator(); + assertThat(validator.listIssues(claimStatistic)).containsExactly("UNAPPROVED"); + })); + + result.add(new TestData("", Collections.singletonList(ImmutablePair.of(option, new String[]{"Unapproved:1"})), + basePath -> { + DefaultLog.getInstance().warn("counterMinTest setup for " + basePath); + File baseDir = basePath.toFile(); + writeFile(baseDir, "Test.java", Arrays.asList("/*\n", "SPDX-License-Identifier: Unapproved\n", + "*/\n\n", "class Test {}\n")); + }, + validatorData -> { + DefaultLog.getInstance().warn("validating counterMinTest for " + validatorData.getBaseDir()); + ClaimStatistic claimStatistic = validatorData.getStatistic(); + ClaimValidator validator = validatorData.getConfiguration().getClaimValidator(); + assertThat(validator.listIssues(claimStatistic)).isEmpty(); + })); + } + + /** + * Add results to the result list. + * + * @param result the result list. + * @param option configuration option we are testing. + */ + protected void configTest(final Set result, final Option option) { + Consumer setup = mkRat.andThen(basePath -> { + DefaultLog.getInstance().warn("configTest setup for " + basePath); + Path ratDir = basePath.resolve(".rat"); + Path oneXml = ratDir.resolve("One.xml"); + DataUtils.generateTextConfig(oneXml, "ONE", "one"); + + Path twoXml = ratDir.resolve("Two.xml"); + DataUtils.generateTextConfig(twoXml, "TWO", "two"); + + File baseDir = basePath.toFile(); + writeFile(baseDir, "bsd.txt", "SPDX-License-Identifier: BSD-3-Clause"); + writeFile(baseDir, "one.txt", "one is the loneliest number"); + }); + String[] args = {".rat/One.xml", ".rat/Two.xml"}; + + ImmutablePair underTest = ImmutablePair.of(option, args); + + result.add(new TestData(DataUtils.asDirName(option), NO_OPTIONS, + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating configTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 2); + assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 1); + assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 1); + })); + + result.add(new TestData("withDefaults", Collections.singletonList(underTest), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating configTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 2); + assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 2); + assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 0); + })); + + if (!Arg.CONFIGURATION_NO_DEFAULTS.isEmpty()) { + result.add(new TestData("noDefaults", Arrays.asList(underTest, + ImmutablePair.of(Arg.CONFIGURATION_NO_DEFAULTS.find("configuration-no-defaults"), null)), + setup, + /** Make validator data a structure with counter countes and file checks. */ + validatorData -> { + DefaultLog.getInstance().warn("validating configTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 2); + assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 1); + assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 1); + })); + } + } + + protected void configurationNoDefaultsTest(final Set result, final Option option) { + TestData test1 = new TestData("", Collections.singletonList(ImmutablePair.of(option, null)), + basePath -> { + DefaultLog.getInstance().warn("configurationNoDefaultsTest setup for " + basePath); + File baseDir = basePath.toFile(); + writeFile(baseDir, "Test.java", Arrays.asList("/*\n", "SPDX-License-Identifier: Apache-2.0\n", + "*/\n\n", "class Test {}\n")); + }, + validatorData -> { + DefaultLog.getInstance().warn("validating configurationNoDefaultsTest for " + validatorData.getBaseDir()); + assertThat(validatorData.getConfiguration().getLicenses(LicenseSetFactory.LicenseFilter.ALL)).isEmpty(); + }); + test1.setException(NO_LICENSES_EXCEPTION); + result.add(test1); + } + + protected void dryRunTest(final Set result, final Option option) { + result.add(new TestData("stdRun", Collections.singletonList(ImmutablePair.of(option, null)), + NO_SETUP, + validatorData -> { + DefaultLog.getInstance().warn("validating dryRunTest for " + validatorData.getBaseDir()); + assertThat(validatorData.getConfiguration().isDryRun()).as("testing getConfiguration().isDryRun() flag").isTrue(); + })); + + result.add(new TestData(DataUtils.asDirName(option), NO_OPTIONS, + NO_SETUP, + validatorData -> { + DefaultLog.getInstance().warn("validating dryRunTest for " + validatorData.getBaseDir()); + assertThat(validatorData.getConfiguration().isDryRun()).as("testing getConfiguration().isDryRun() flag").isFalse(); + })); + } + + protected void editCopyrightTest(final Set result, final Option option) { + Consumer setup = basePath -> { + DefaultLog.getInstance().warn("editCopyrightTest setup for " + basePath); + File baseDir = basePath.toFile(); + writeFile(baseDir, "Missing.java", Arrays.asList("/* no license */\n\n", "class Test {}\n")); + }; + ImmutablePair copyright = ImmutablePair.of(option, new String[]{"MyCopyright"}); + if (Arg.EDIT_ADD.isEmpty()) { + throw new RuntimeException("Can not execute copyright tests without an EDIT_ADD option avialable"); + } + ImmutablePair editLicense = ImmutablePair.of(Arg.EDIT_ADD.option(), null); + + result.add(new TestData("noEditLicense", Collections.singletonList(copyright), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating editCopyrightTest for " + validatorData.getBaseDir()); + try { + String actualText = TextUtils.readFile(validatorData.getBaseDir().resolve("Missing.java").toFile()); + TextUtils.assertNotContains("MyCopyright", actualText); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + })); + + result.add(new TestData(DataUtils.asDirName(editLicense.getLeft()), Arrays.asList(copyright, editLicense), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating editCopyrightTest for " + validatorData.getBaseDir()); + try { + String actualText = TextUtils.readFile(validatorData.getBaseDir().resolve("Missing.java").toFile()); + TextUtils.assertNotContains("MyCopyright", actualText); + assertThat(validatorData.getBaseDir().resolve("Missing.java.new")).exists(); + actualText = TextUtils.readFile(validatorData.getBaseDir().resolve("Missing.java.new").toFile()); + TextUtils.assertContains("MyCopyright", actualText); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + })); + + if (!Arg.DRY_RUN.isEmpty()) { + Option dryRun = Arg.DRY_RUN.option(); + result.add(new TestData(DataUtils.asDirName(dryRun), + Arrays.asList(copyright, ImmutablePair.of(dryRun, null), editLicense), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating editCopyrightTest for " + validatorData.getBaseDir()); + try { + String actualText = TextUtils.readFile(validatorData.getBaseDir().resolve("Missing.java").toFile()); + TextUtils.assertNotContains("MyCopyright", actualText); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + assertThat(validatorData.getBaseDir().resolve("Missing.java.new")).doesNotExist(); + })); + } + + if (!Arg.EDIT_OVERWRITE.isEmpty()) { + Option overwrite = Arg.EDIT_OVERWRITE.option(); + result.add(new TestData(DataUtils.asDirName(overwrite), Arrays.asList(copyright, editLicense, ImmutablePair.of(overwrite, null)), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating editCopyrightTest for " + validatorData.getBaseDir()); + try { + String actualText = TextUtils.readFile(validatorData.getBaseDir().resolve("Missing.java").toFile()); + TextUtils.assertContains("MyCopyright", actualText); + assertThat(validatorData.getBaseDir().resolve("Missing.java.new")).doesNotExist(); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + })); + } + } + + protected void editLicenseTest(final Set result, final Option option) { + result.add(new TestData("", Collections.singletonList(ImmutablePair.of(option, null)), + basePath -> { + DefaultLog.getInstance().warn("editLicenseTest setup for " + basePath); + File baseDir = basePath.toFile(); + writeFile(baseDir, "NoLicense.java", "class NoLicense {}"); + }, + validatorData -> { + DefaultLog.getInstance().warn("validating editLicenseTest for " + validatorData.getBaseDir()); + try { + assertThat(validatorData.getStatistic()).isNotNull(); + File javaFile = validatorData.getBaseDir().resolve("NoLicense.java").toFile(); + String contents = String.join("\n", IOUtils.readLines(new FileReader(javaFile, StandardCharsets.UTF_8))); + assertThat(contents).isEqualTo("class NoLicense {}"); + File resultFile = validatorData.getBaseDir().resolve("NoLicense.java.new").toFile(); + assertThat(resultFile).exists(); + contents = String.join("\n", IOUtils.readLines(new FileReader(resultFile, StandardCharsets.UTF_8))); + assertThat(contents).isEqualTo(""" + /* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + *\s + * http://www.apache.org/licenses/LICENSE-2.0 + *\s + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + + class NoLicense {}"""); + } catch (IOException e) { + throw new RuntimeException(e); + } + })); + } + + protected void editOverwriteTest(final Set result, final Option option) { + result.add(new TestData("noEditLicense", Collections.singletonList(ImmutablePair.of(option, null)), + NO_SETUP, + validatorData -> { + DefaultLog.getInstance().warn("validating editOverwriteTest for " + validatorData.getBaseDir()); + assertThat(validatorData.getConfiguration().isAddingLicensesForced()) + .describedAs("Without edit-license should be false").isFalse(); + })); + + if (!Arg.EDIT_ADD.isEmpty()) { + result.add(new TestData("", Arrays.asList(ImmutablePair.of(option, null), + ImmutablePair.of(Arg.EDIT_ADD.find("edit-license"), null)), + NO_SETUP, + validatorData -> { + DefaultLog.getInstance().warn("validating editOverwriteTest for " + validatorData.getBaseDir()); + assertThat(validatorData.getConfiguration().isAddingLicensesForced()) + .as("testing getConfiguration().isAddingLicensesForced() flag").isTrue(); + })); + } + } + + protected void logLevelTest(final Set result, final Option option) { + final TestingLog testingLog = new TestingLog(); + + Consumer setup = basePath -> { + DefaultLog.getInstance().warn("logLevelTest setup for " + basePath); + DefaultLog.setInstance(testingLog); + testingLog.clear(); + }; + + result.add(new TestData(Log.Level.INFO.name(), + Collections.singletonList(ImmutablePair.of(option, new String[]{Log.Level.INFO.name()})), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating logLevelTest for " + validatorData.getBaseDir()); + try { + testingLog.assertNotContains("DEBUG"); + } finally { + DefaultLog.setInstance(null); + } + })); + + result.add(new TestData(Log.Level.DEBUG.name(), + Collections.singletonList(ImmutablePair.of(option, new String[]{Log.Level.DEBUG.name()})), + setup, + validatorData -> { + DefaultLog.getInstance().warn("validating logLevelTest for " + validatorData.getBaseDir()); + try { + testingLog.assertContains("DEBUG"); + } finally { + DefaultLog.setInstance(null); + } + })); + } + + protected void outputArchiveTest(final Set result, final Option option) { + for (ReportConfiguration.Processing processing : ReportConfiguration.Processing.values()) { + result.add(new TestData(processing.name().toLowerCase(Locale.ROOT), + Collections.singletonList(ImmutablePair.of(option, new String[]{processing.name()})), + basePath -> { + DefaultLog.getInstance().warn("outputArchiveTest setup for " + basePath); + File localArchive = new File(basePath.toFile(), "dummy.jar"); + try (InputStream in = ReportTestDataProvider.class.getResourceAsStream("/tikaFiles/archive/dummy.jar"); + OutputStream out = Files.newOutputStream(localArchive.toPath())) { + Objects.requireNonNull(in); + IOUtils.copy(in, out); + } catch (IOException e) { + throw new RuntimeException(e); + } + }, + validatorData -> { + DefaultLog.getInstance().warn("validating outputArchiveTest for " + validatorData.getBaseDir()); + Document document = validatorData.getDocument(); + try { + XmlUtils.assertIsPresent(processing.name(), document, xpath, "/rat-report/resource[@name='/dummy.jar']"); + switch (processing) { + case ABSENCE: + XmlUtils.assertIsPresent(processing.name(), document, xpath, "/rat-report/resource[@name='/dummy.jar']/license[@family='AL ']"); + XmlUtils.assertIsPresent(processing.name(), document, xpath, "/rat-report/resource[@name='/dummy.jar']/license[@family='?????']"); + break; + case PRESENCE: + XmlUtils.assertIsPresent(processing.name(), document, xpath, "/rat-report/resource[@name='/dummy.jar']/license[@family='AL ']"); + XmlUtils.assertIsNotPresent(processing.name(), document, xpath, "/rat-report/resource[@name='/dummy.jar']/license[@family='?????']"); + break; + case NOTIFICATION: + XmlUtils.assertIsNotPresent(processing.name(), document, xpath, "/rat-report/resource[@name='/dummy.jar']/license[@family='AL ']"); + XmlUtils.assertIsNotPresent(processing.name(), document, xpath, "/rat-report/resource[@name='/dummy.jar']/license[@family='?????']"); + break; + default: + throw new IllegalArgumentException("Unexpected processing " + processing); + } + } catch (XPathExpressionException e) { + throw new RuntimeException(e); + } + }) + ); + } + } + + protected void outputFamiliesTest(final Set result, final Option option) { + for (LicenseSetFactory.LicenseFilter filter : LicenseSetFactory.LicenseFilter.values()) { + result.add(new TestData(filter.name().toLowerCase(Locale.ROOT), + Collections.singletonList(ImmutablePair.of(option, new String[]{filter.name()})), + NO_SETUP, + validatorData -> { + DefaultLog.getInstance().warn("validating outputFamiliesTest for " + validatorData.getBaseDir()); + Document document = validatorData.getDocument(); + try { + switch (filter) { + case ALL: + XmlUtils.assertIsPresent(filter.name(), document, xpath, "/rat-report/rat-config/families/family[@id='AL']"); + XmlUtils.assertIsPresent(filter.name(), document, xpath, "/rat-report/rat-config/families/family[@id='GPL']"); + break; + case APPROVED: + XmlUtils.assertIsPresent(filter.name(), document, xpath, "/rat-report/rat-config/families/family[@id='AL']"); + XmlUtils.assertIsNotPresent(filter.name(), document, xpath, "/rat-report/rat-config/families/family[@id='GPL']"); + break; + case NONE: + XmlUtils.assertIsNotPresent(filter.name(), document, xpath, "/rat-report/rat-config/families/family[@id='AL']"); + XmlUtils.assertIsNotPresent(filter.name(), document, xpath, "/rat-report/rat-config/families/family[@id='GPL']"); + break; + default: + throw new IllegalArgumentException("Unexpected filter: " + filter); + } + } catch (XPathExpressionException e) { + throw new RuntimeException(e); + } + }) + ); + } + } + + protected void outputFileTest(final Set result, final Option option) { + result.add(new TestData("", Collections.singletonList(ImmutablePair.of(option, new String[]{"outexample"})), + basePath -> { + DefaultLog.getInstance().warn("outputFileTest setup for " + basePath); + File baseDir = basePath.toFile(); + writeFile(baseDir, "apl.txt", "SPDX-License-Identifier: Apache-2.0"); + }, + validatorData -> { + DefaultLog.getInstance().warn("validating outputFamiliesTest for " + validatorData.getBaseDir()); + assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 1); + assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 1); + assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 0); + File outFile = validatorData.getBaseDir().resolve("outexample").toFile(); + + try { + String actualText = TextUtils.readFile(outFile); + assertThat(actualText).containsPattern("Apache License 2.0: \\d") + .containsPattern("STANDARD:\\s+\\d"); + } catch (IOException e) { + throw new RuntimeException(e); + } + }) + ); + } + + protected void outputLicensesTest(final Set result, final Option option) { + for (LicenseSetFactory.LicenseFilter filter : LicenseSetFactory.LicenseFilter.values()) { + result.add(new TestData(filter.name(), Collections.singletonList(ImmutablePair.of(option, new String[]{filter.name()})), + NO_SETUP, + validatorData -> { + DefaultLog.getInstance().warn("validating outputLicensesTest for " + validatorData.getBaseDir()); + Document document = validatorData.getDocument(); + try { + switch (filter) { + case ALL: + XmlUtils.assertIsPresent(filter.name(), document, xpath, "/rat-report/rat-config/licenses/license[@id='AL2.0']"); + XmlUtils.assertIsPresent(filter.name(), document, xpath, "/rat-report/rat-config/licenses/license[@id='GPL1']"); + break; + case APPROVED: + XmlUtils.assertIsPresent(filter.name(), document, xpath, "/rat-report/rat-config/licenses/license[@id='AL2.0']"); + XmlUtils.assertIsNotPresent(filter.name(), document, xpath, "/rat-report/rat-config/licenses/license[@id='GPL1']"); + break; + case NONE: + XmlUtils.assertIsNotPresent(filter.name(), document, xpath, "/rat-report/rat-config/licenses/license[@id='AL2.0']"); + XmlUtils.assertIsNotPresent(filter.name(), document, xpath, "/rat-report/rat-config/licenses/license[@id='GPL1']"); + break; + default: + throw new IllegalArgumentException("Unexpected filter: " + filter); + } + } catch (XPathExpressionException e) { + throw new RuntimeException(e); + } + })); + } + } + + + protected void outputStandardTest(final Set result, final Option option) { + for (ReportConfiguration.Processing proc : ReportConfiguration.Processing.values()) { + result.add(new TestData(proc.name().toLowerCase(Locale.ROOT), Collections.singletonList(ImmutablePair.of(option, new String[]{proc.name()})), + basePath -> { + DefaultLog.getInstance().warn("outputStandardTest setup for " + basePath); + File baseDir = basePath.toFile(); + writeFile(baseDir, "Test.java", Arrays.asList("/*\n", "SPDX-License-Identifier: Apache-2.0\n", + "*/\n\n", "class Test {}\n")); + writeFile(baseDir, "Missing.java", Arrays.asList("/* no license */\n\n", "class Test {}\n")); + }, + validatorData -> { + DefaultLog.getInstance().warn("validating outputStandardTest for " + validatorData.getBaseDir()); + Document document = validatorData.getDocument(); + String testDoc = "/rat-report/resource[@name='/Test.java']"; + String missingDoc = "/rat-report/resource[@name='/Missing.java']"; + try { + XmlUtils.assertIsPresent(proc.name(), document, xpath, testDoc); + XmlUtils.assertIsPresent(proc.name(), document, xpath, missingDoc); + + switch (proc) { + case ABSENCE: + XmlUtils.assertIsPresent(proc.name(), document, xpath, testDoc + "/license[@family='AL ']"); + XmlUtils.assertIsPresent(proc.name(), document, xpath, missingDoc + "/license[@family='?????']"); + break; + case PRESENCE: + XmlUtils.assertIsPresent(proc.name(), document, xpath, testDoc + "/license[@family='AL ']"); + XmlUtils.assertIsNotPresent(proc.name(), document, xpath, missingDoc + "/license[@family='?????']"); + break; + case NOTIFICATION: + XmlUtils.assertIsNotPresent(proc.name(), document, xpath, testDoc + "/license[@family='AL ']"); + XmlUtils.assertIsNotPresent(proc.name(), document, xpath, missingDoc + "/license[@family='?????']"); + break; + default: + throw new IllegalArgumentException("Unexpected processing " + proc); + } + } catch (XPathExpressionException ex) { + throw new RuntimeException(ex); + } + })); + } + } + + + protected void outputStyleTest(final Set result, final Option option) { + Consumer createFile = basePath -> { + DefaultLog.getInstance().warn("outputStyleTest setup for " + basePath); + File baseDir = basePath.toFile(); + writeFile(baseDir, "Test.java", Arrays.asList("/*\n", "SPDX-License-Identifier: Apache-2.0\n", + "*/\n\n", "class Test {}\n")); + writeFile(baseDir, "Missing.java", Arrays.asList("/* no license */\n\n", "class Test {}\n")); + }; + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + if (!option.hasArg()) { + if (option.getLongOpt().equals("xml")) { + result.add(new TestData("", Collections.singletonList(ImmutablePair.of(option, null)), + createFile, + validatorData -> { + DefaultLog.getInstance().warn("validating outputStyleTest for " + validatorData.getBaseDir()); + try (InputStream expected = StyleSheets.XML.getStyleSheet().ioSupplier().get(); + InputStream actual = validatorData.getConfiguration().getStyleSheet().get()) { + assertThat(IOUtils.contentEquals(expected, actual)).as(() -> String.format("'%s' does not match", StyleSheets.XML)).isTrue(); + baos.reset(); + validatorData.getOutput().format(validatorData.getConfiguration().getStyleSheet(), () -> baos); + String actualText = baos.toString(); + TextUtils.assertContainsExactly(1, "", actualText); + TextUtils.assertContainsExactly(1, "", actualText); + } catch (IOException | RatException e) { + throw new RuntimeException(e); + } + })); + } + } else { + for (StyleSheets sheet : StyleSheets.values()) { + result.add(new TestData(sheet.name().toLowerCase(Locale.ROOT), Collections.singletonList(ImmutablePair.of(option, new String[]{sheet.arg()})), + createFile, + validatorData -> { + DefaultLog.getInstance().warn("validating outputStyleTest for " + validatorData.getBaseDir()); + try (InputStream expected = sheet.getStyleSheet().ioSupplier().get(); + InputStream actual = validatorData.getConfiguration().getStyleSheet().get()) { + assertThat(IOUtils.contentEquals(expected, actual)).as(() -> String.format("'%s' does not match", sheet)).isTrue(); + baos.reset(); + validatorData.getOutput().format(validatorData.getConfiguration().getStyleSheet(), () -> baos); + String actualText = baos.toString(); + switch (sheet) { + case MISSING_HEADERS: + TextUtils.assertContainsExactly(1, "Files with missing headers:" + System.lineSeparator() + + " /Missing.java", actualText); + break; + case PLAIN: + TextUtils.assertContainsExactly(1, "Unknown license: 1 ", actualText); + TextUtils.assertContainsExactly(1, "?????: 1 ", actualText); + break; + case XML: + TextUtils.assertContainsExactly(1, "", actualText); + TextUtils.assertContainsExactly(1, "", actualText); + break; + case UNAPPROVED_LICENSES: + TextUtils.assertContainsExactly(1, "Files with unapproved licenses:" + System.lineSeparator() + " /Missing.java", actualText); + break; + case XHTML5: + TextUtils.assertPatternInTarget("Approved<\\/td>\\s+\\d+<\\/td>\\s+A count of approved licenses.<\\/td>", actualText); + break; + default: + fail("No test for stylesheet " + sheet); + break; + } + } catch (IOException | RatException e) { + throw new RuntimeException(e); + } + })); + } + + result.add(new TestData("fileStyleSheet", + Collections.singletonList(ImmutablePair.of(option, new String[]{"fileStyleSheet.xslt"})), + basePath -> { + DefaultLog.getInstance().warn("outputStyleTest setup for " + basePath); + DocumentName name = DocumentName.builder().setName("fileStyleSheet.xslt") + .setBaseName(basePath.toString()).build(); + assertDoesNotThrow(() -> { + try (FileWriter fileWriter = new FileWriter(name.asFile(), StandardCharsets.UTF_8); + XmlWriter writer = new XmlWriter(fileWriter)) { + writer.startDocument() + .comment(DataUtils.ASF_TEXT) + .startElement("xsl:stylesheet") + .attribute("version", "1.0") + .attribute("xmlns:xsl", "http://www.w3.org/1999/XSL/Transform") + .startElement("xsl:template") + .attribute("match", "@*|node()") + .content("Hello World") + .closeDocument(); + } + }); + }, + + validatorData -> { + DefaultLog.getInstance().warn("validating outputStyleTest for " + validatorData.getBaseDir()); + try (InputStream expected = StyleSheets.getStyleSheet("fileStyleSheet.xslt", validatorData.getBaseName()).ioSupplier().get(); + InputStream actual = validatorData.getConfiguration().getStyleSheet().get()) { + assertThat(IOUtils.contentEquals(expected, actual)).as(() -> "'fileStyleSheet.xslt' does not match").isTrue(); + baos.reset(); + validatorData.getOutput().format(validatorData.getConfiguration().getStyleSheet(), () -> baos); + String actualText = baos.toString(); + TextUtils.assertContainsExactly(1, "Hello World", actualText); + } catch (IOException | RatException e) { + throw new RuntimeException(e); + } + })); + } + } +} diff --git a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/TestData.java b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/TestData.java new file mode 100644 index 000000000..84c8c213f --- /dev/null +++ b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/TestData.java @@ -0,0 +1,232 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + */ +package org.apache.rat.testhelpers.data; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import org.apache.commons.cli.Option; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.text.WordUtils; +import org.apache.rat.ui.ArgumentTracker; +import org.apache.rat.utils.CasedString; +import org.apache.rat.utils.FileUtils; + +/** + * The definition of a test. + */ +public final class TestData implements Comparable { + /** if set, the expected exception from the test. */ + private Exception expectedException; + /** The sub name of the test */ + private final String name; + /** The command line for the test */ + private final List> commandLine; + /** A function to set up the test in a specific path */ + private final Consumer setupFiles; + /** A function to test the results of the test. */ + private final Consumer validator; + + /** + * Constructs the Test data + * @param name the sub name of the test. May not be {@code null} but may be an empty string. Should + * be specified in Camel case for multiple words. + * @param commandLine The command line for the test. May not be {@code null} but may consist of a single {@link ImmutablePair#nullPair()}. + * @param setupFiles the method to set up the files for the test. May not be {@code null}. + * @param validator the validator for the results of the test. May not be {@code null}. + */ + public TestData(String name, List> commandLine, + Consumer setupFiles, + Consumer validator) { + Objects.requireNonNull(name, " name cannot be null"); + Objects.requireNonNull(commandLine, "commandLine cannot be null"); + Objects.requireNonNull(setupFiles, "setupFiles cannot be null"); + Objects.requireNonNull(validator, "validator cannot be null"); + if (name.contains("/")) { + throw new IllegalArgumentException("name may not contain '/', use camel case instead"); + } + if (commandLine.isEmpty()) { + throw new IllegalArgumentException("commandLine may not be empty but contain an ImmutablePair.nullPair()"); + } + this.name = name; + this.commandLine = commandLine; + this.setupFiles = setupFiles; + this.validator = validator; + this.expectedException = null; + } + + @Override + public String toString() { + return getTestName(); + } + + /** + * Sets the exception expected from this test. + * @param expectedException the expected exception. + */ + void setException(Exception expectedException) { + this.expectedException = expectedException; + } + + /** + * The option for the test. This is the first option specified in the command line. + * If the command line is empty this returns {@code null}. + * @return the first option in the command line or {@code null} if there is no option. + */ + public Option getOption() { + return commandLine.get(0).getLeft(); + } + + /** + * Get the sub name for this test. + * @return the sub name for this test. + */ + public String getName() { + return name; + } + + /** + * Gets the expected exception or {@code null} if not exception is expected. + * @return the expected exception or {@code null} if not exception is expected. + */ + public Exception getExpectedException() { + return expectedException; + } + + /** + * Determines if the test is expecting an exception. + * @return {@code true} if the test is expecting to throw an exception. + */ + public boolean expectingException() { + return expectedException != null; + } + + /** + * Gets the command line as the string objects that are normally parsed by the + * command line parser. + * @return the command line strings. + */ + public String[] getCommandLine() { + return getCommandLine(null); + } + + /** + * Gets the command line as the string objects that are normally parsed by the + * command line parser. The result will include "--" to terminate a trailing multi + * argument option. + * @param workingDir the directory to add to the command line. May be {@code null}. + * @return the command line strings. + */ + public String[] getCommandLine(String workingDir) { + List args = new ArrayList<>(commandLine.size()); + final boolean[] lastWasMultiArg = {false}; + commandLine.forEach(pair -> { + if (pair.getKey() != null) { + if (pair.getKey().hasLongOpt()) { + args.add("--" + pair.getKey().getLongOpt()); + } else { + args.add("-" + pair.getKey().getOpt()); + } + if (pair.getValue() != null) { + args.addAll(Arrays.asList(pair.getValue())); + } + lastWasMultiArg[0] = pair.getKey().hasArgs(); + } else { + lastWasMultiArg[0] = false; + } + }); + if (lastWasMultiArg[0]) { + args.add("--"); + } + if (workingDir != null) { + args.add(workingDir); + } + return args.toArray(new String[0]); + } + + /** + * Get the arguments for the command line. + * @return the arguments for the command line as Option, String[] pairs. + */ + public List extends Pair> getArgs() { + return commandLine; + } + + /** + * Gets the validator for the test. + * @return the validator for the test. + */ + public Consumer getValidator() { + return validator; + } + + /** + * Sets up the files for the test. + * @param path the path to use as the base directory. Subdirectories and files may be added + * to this path. + */ + public void setupFiles(Path path) { + FileUtils.mkDir(path.toFile()); + setupFiles.accept(path); + } + + /** + * Gets the test name. This is the option concatenated with the name. + * @return the unique test name + */ + public String getTestName() { + new ArrayList<>(); + String result = null; + if (getOption() == null) { + result = name + "_DefaultTest"; + } else { + result = new CasedString(CasedString.StringCase.KEBAB, ArgumentTracker.extractKey(getOption())).toString(); + if (!name.isEmpty()) { + result += "/" + name; + } + } + return result; + } + + /** + * Gets the test name as a class name. This is based on the option concatenated with the name. + * @return the unique Java class name + */ + public String getClassName() { + if (getOption() == null) { + return WordUtils.capitalize(name) + "_DefaultTest"; + } else { + List parts = new ArrayList<>(Arrays.asList(new CasedString(CasedString.StringCase.KEBAB, ArgumentTracker.extractKey(getOption())).getSegments())); + if (!name.isEmpty()) { + parts.addAll(Arrays.asList(new CasedString(CasedString.StringCase.CAMEL, name).getSegments())); + } + return CasedString.StringCase.PASCAL.assemble(parts.toArray(new String[0])); + } + } + + @Override + public int compareTo(TestData other) { + return getTestName().compareTo(other.getTestName()); + } + +} diff --git a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/ValidatorData.java b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/ValidatorData.java new file mode 100644 index 000000000..a4745fe4f --- /dev/null +++ b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/ValidatorData.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + */ +package org.apache.rat.testhelpers.data; + +import java.nio.file.Path; +import java.nio.file.Paths; +import org.apache.rat.ReportConfiguration; +import org.apache.rat.Reporter; +import org.apache.rat.document.DocumentName; +import org.apache.rat.report.claim.ClaimStatistic; +import org.w3c.dom.Document; + +/** + * The validator for test data. + */ +public final class ValidatorData { + /** The report output */ + private final Reporter.Output output; + /** the base directory where the test setup was created */ + private final Path baseDir; + + /** + * Constructor. + * @param output The report output. + * @param baseDir the directory where the test setup was created. + */ + public ValidatorData(final Reporter.Output output, final String baseDir) { + this.output = output; + this.baseDir = Paths.get(baseDir); + } + + /** + * Gets the directory where the test setup was created as a DocumentName. + * @return the DocumentName for the baseDir directory. + */ + public DocumentName getBaseName() { + return DocumentName.builder(baseDir.toFile()).build(); + } + + /** + * Creates a DocumentName for a file name. The root of the document Name will be the baseDir. + * @param fileName the file name to create a document name for. + * @return the Document name for the file. + */ + public DocumentName mkDocName(String fileName) { + return DocumentName.builder().setBaseName(baseDir.toFile()).setName(fileName).build(); + } + + public Reporter.Output getOutput() { + return output; + } + /** + * Gets the document that was generated during execution. + * @return the document that was generated during execution. + */ + public Document getDocument() { + return output.getDocument(); + } + + /** + * Gets the claim statistics from the run + * @return the ClaimStatistic from the run. + */ + public ClaimStatistic getStatistic() { + return output.getStatistic(); + } + + /** + * Gets the configuration from the run. + * @return the configuration from the run. + */ + public ReportConfiguration getConfiguration() { + return output.getConfiguration(); + } + + public Path getBaseDir() { + return baseDir; + } + +} diff --git a/apache-rat-core/src/test/java/org/apache/rat/utils/StandardXmlFactoryTests.java b/apache-rat-core/src/test/java/org/apache/rat/utils/StandardXmlFactoryTests.java index bfffac4fa..28e36a80a 100644 --- a/apache-rat-core/src/test/java/org/apache/rat/utils/StandardXmlFactoryTests.java +++ b/apache-rat-core/src/test/java/org/apache/rat/utils/StandardXmlFactoryTests.java @@ -18,14 +18,22 @@ */ package org.apache.rat.utils; +import org.apache.rat.report.xml.writer.XmlWriter; import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.InputStream; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -33,7 +41,25 @@ /** * Tests for StandardXmlFactory. */ -class StandardXmlFactoryTests { +public class StandardXmlFactoryTests { + + public static final String SIMPLE_DOCUMENT_TEXT = """ + + + + """; + + public static Document simpleDocument() throws IOException, SAXException { + StringWriter writer = new StringWriter(); + try (XmlWriter xmlWriter = new XmlWriter(writer)) { + xmlWriter.startDocument().startElement("first") + .startElement("second") + .attribute("attr1", "one") + .closeDocument(); + } + return StandardXmlFactory.documentBuilder() + .parse(new ByteArrayInputStream(writer.toString().getBytes(StandardCharsets.UTF_8))); + } @Test void noArg() throws TransformerConfigurationException { @@ -64,6 +90,18 @@ void goodDocumentBuilderTest() { .isNotNull(); } + @Test + void serializeDocumentTest() throws IOException, SAXException, TransformerException { + String result = StandardXmlFactory.serializeDocument(simpleDocument()); + assertThat(result).isEqualToIgnoringNewLines(SIMPLE_DOCUMENT_TEXT); + } + + @Test + void serializeEmptyDocumentYieldsEmtpyString() throws TransformerException { + Document document = StandardXmlFactory.documentBuilder().newDocument(); + assertThat(StandardXmlFactory.serializeDocument(document)).isEmpty(); + } + /** * Class to test failing document builder. */ diff --git a/apache-rat-core/src/test/resources/XmlOutputExamples/elements.xml b/apache-rat-core/src/test/resources/XmlOutputExamples/elements.xml index 4f1c340f9..b2ca96372 100644 --- a/apache-rat-core/src/test/resources/XmlOutputExamples/elements.xml +++ b/apache-rat-core/src/test/resources/XmlOutputExamples/elements.xml @@ -1,4 +1,8 @@ + @@ -8,16 +12,6 @@ - diff --git a/apache-rat-plugin/src/it/RAT-469/invoker.properties b/apache-rat-plugin/src/it/RAT-469/invoker.properties index 6486eb1de..0e632cdef 100644 --- a/apache-rat-plugin/src/it/RAT-469/invoker.properties +++ b/apache-rat-plugin/src/it/RAT-469/invoker.properties @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -invoker.goals = clean apache-rat:rat +invoker.goals = clean apache-rat:check apache-rat:rat diff --git a/apache-rat-plugin/src/main/java/org/apache/rat/mp/RatCheckMojo.java b/apache-rat-plugin/src/main/java/org/apache/rat/mp/RatCheckMojo.java index 5a55a0612..d400b53cc 100644 --- a/apache-rat-plugin/src/main/java/org/apache/rat/mp/RatCheckMojo.java +++ b/apache-rat-plugin/src/main/java/org/apache/rat/mp/RatCheckMojo.java @@ -36,6 +36,7 @@ import org.apache.rat.commandline.Arg; import org.apache.rat.commandline.StyleSheets; import org.apache.rat.config.exclusion.StandardCollection; +import org.apache.rat.config.results.ClaimValidator; import org.apache.rat.license.LicenseSetFactory.LicenseFilter; import org.apache.rat.report.claim.ClaimStatistic; import org.apache.rat.utils.DefaultLog; @@ -158,9 +159,6 @@ public void setCopyrightMessage(final String copyrightMessage) { @Parameter(property = "rat.consoleOutput", defaultValue = "true") private boolean consoleOutput; - /** The reporter that this mojo uses */ - private Reporter reporter; - @Override protected ReportConfiguration getConfiguration() throws MojoExecutionException { ReportConfiguration result = super.getConfiguration(); @@ -208,12 +206,12 @@ public void execute() throws MojoExecutionException, MojoFailureException { config.reportExclusions(logWriter); } try { - this.reporter = new Reporter(config); - reporter.output(); + Reporter.Output output = new Reporter(config).execute(); if (verbose) { - reporter.writeSummary(logWriter); + output.writeSummary(logWriter); } - check(config); + output.format(config); + check(output); } catch (MojoFailureException e) { throw e; } catch (Exception e) { @@ -224,17 +222,18 @@ public void execute() throws MojoExecutionException, MojoFailureException { } } - protected void check(final ReportConfiguration config) throws MojoFailureException { - ClaimStatistic statistics = reporter.getClaimsStatistic(); + protected void check(final Reporter.Output output) throws MojoFailureException { + ClaimStatistic statistics = output.getStatistic(); + ClaimValidator validator = output.getConfiguration().getClaimValidator(); try { - reporter.writeSummary(DefaultLog.getInstance().asWriter(Log.Level.DEBUG)); - if (config.getClaimValidator().hasErrors()) { - config.getClaimValidator().logIssues(statistics); + output.writeSummary(DefaultLog.getInstance().asWriter(Log.Level.DEBUG)); + if (validator.hasErrors()) { + validator.logIssues(statistics); if (consoleOutput && - !config.getClaimValidator().isValid(ClaimStatistic.Counter.UNAPPROVED, statistics.getCounter(ClaimStatistic.Counter.UNAPPROVED))) { + !validator.isValid(ClaimStatistic.Counter.UNAPPROVED, statistics.getCounter(ClaimStatistic.Counter.UNAPPROVED))) { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - reporter.output(StyleSheets.UNAPPROVED_LICENSES.getStyleSheet().ioSupplier(), () -> baos); + output.format(StyleSheets.UNAPPROVED_LICENSES.getStyleSheet().ioSupplier(), () -> baos); getLog().warn(baos.toString(StandardCharsets.UTF_8)); } catch (RuntimeException rte) { throw rte; @@ -244,7 +243,7 @@ protected void check(final ReportConfiguration config) throws MojoFailureExcepti } String msg = format("Counter(s) %s exceeded minimum or maximum values. See RAT report in: '%s'.", - String.join(", ", config.getClaimValidator().listIssues(statistics)), + String.join(", ", validator.listIssues(statistics)), getRatTxtFile()); if (!ignoreErrors) { diff --git a/apache-rat-plugin/src/main/java/org/apache/rat/mp/RatReportMojo.java b/apache-rat-plugin/src/main/java/org/apache/rat/mp/RatReportMojo.java index 99ac97f58..23e7a990c 100644 --- a/apache-rat-plugin/src/main/java/org/apache/rat/mp/RatReportMojo.java +++ b/apache-rat-plugin/src/main/java/org/apache/rat/mp/RatReportMojo.java @@ -441,11 +441,12 @@ protected void executeReport(final Locale locale) throws MavenReportException { config.reportExclusions(logWriter); } ByteArrayOutputStream baos = new ByteArrayOutputStream(); - config.setOut(new ReportConfiguration.IODescriptor("RAT output", () -> baos)); + config.setOut(new ReportConfiguration.IODescriptor<>("RAT output", () -> baos)); Reporter reporter = new Reporter(config); - reporter.output(); + Reporter.Output output = reporter.execute(); + output.format(config); if (verbose) { - reporter.writeSummary(logWriter); + output.writeSummary(logWriter); } sink.text(baos.toString(StandardCharsets.UTF_8.name())); } catch (IOException | MojoExecutionException | RatException e) { diff --git a/apache-rat-plugin/src/test/java/org/apache/rat/mp/RatCheckMojoTest.java b/apache-rat-plugin/src/test/java/org/apache/rat/mp/RatCheckMojoTest.java index 5734ab73e..3013df2ac 100644 --- a/apache-rat-plugin/src/test/java/org/apache/rat/mp/RatCheckMojoTest.java +++ b/apache-rat-plugin/src/test/java/org/apache/rat/mp/RatCheckMojoTest.java @@ -247,7 +247,7 @@ void it5() throws Exception { assertThat(config.isAddingLicenses()).as("Should not be adding licenses").isFalse(); assertThat(config.isAddingLicensesForced()).as("Should not be forcing licenses").isFalse(); - ReportConfigurationTest.validateDefaultApprovedLicenses(config, 1); + ReportConfigurationTest.validateDefaultApprovedLicenses(config, "CC-BY-NC-ND"); assertThat(config.getLicenseCategories(LicenseFilter.APPROVED)).doesNotContain(ILicenseFamily.makeCategory("YAL")) .contains(ILicenseFamily.makeCategory("CC")); ReportConfigurationTest.validateDefaultLicenseFamilies(config, "YAL", "CC"); @@ -332,7 +332,7 @@ void rat343() throws Exception { assertThat(config.getCopyrightMessage()).isNull(); assertThat(config.getStyleSheet()).withFailMessage("Stylesheet should not be null").isNotNull(); - ReportConfigurationTest.validateDefaultApprovedLicenses(config, 1); + ReportConfigurationTest.validateDefaultApprovedLicenses(config, "BSD"); ReportConfigurationTest.validateDefaultLicenseFamilies(config, "BSD", "CC BY"); ReportConfigurationTest.validateDefaultLicenses(config, "BSD", "CC BY"); diff --git a/apache-rat-tasks/pom.xml b/apache-rat-tasks/pom.xml index aca9d90b7..312d9f160 100644 --- a/apache-rat-tasks/pom.xml +++ b/apache-rat-tasks/pom.xml @@ -188,6 +188,25 @@ org.apache.maven.plugins maven-antrun-plugin + + ${skipTests} + + + + + + + + + + + + + + + + + test diff --git a/apache-rat-tasks/src/main/java/org/apache/rat/anttasks/Report.java b/apache-rat-tasks/src/main/java/org/apache/rat/anttasks/Report.java index cd91a8ecd..79f0aa985 100644 --- a/apache-rat-tasks/src/main/java/org/apache/rat/anttasks/Report.java +++ b/apache-rat-tasks/src/main/java/org/apache/rat/anttasks/Report.java @@ -29,7 +29,6 @@ import org.apache.commons.cli.Option; import org.apache.commons.io.filefilter.IOFileFilter; -import org.apache.commons.io.output.CloseShieldOutputStream; import org.apache.rat.ConfigurationException; import org.apache.rat.DeprecationReporter; import org.apache.rat.ImplementationException; @@ -411,14 +410,15 @@ public ReportConfiguration getConfiguration() { try { boolean helpLicenses = !getValues(Arg.HELP_LICENSES).isEmpty(); removeKey(Arg.HELP_LICENSES); - - final ReportConfiguration configuration = OptionCollection.parseCommands(new File("."), args().toArray(new String[0]), + File antFileDir = new File(getProject().getProperty("ant.file")).getParentFile(); + DocumentName name = DocumentName.builder(antFileDir).build(); + final ReportConfiguration configuration = OptionCollection.parseCommands(antFileDir, args().toArray(new String[0]), o -> DefaultLog.getInstance().warn("Help option not supported"), true); if (getValues(Arg.OUTPUT_FILE).isEmpty()) { - configuration.setOut(new ReportConfiguration.IODescriptor<>("RAT output", () -> new LogOutputStream(this, Project.MSG_INFO))); + configuration.setOut(new ReportConfiguration.IODescriptor<>("log output", () -> new LogOutputStream(this, Project.MSG_INFO))); } - DocumentName name = DocumentName.builder(getProject().getBaseDir()).build(); + configuration.addSource(new ResourceCollectionContainer(name, configuration, nestedResources)); configuration.addApprovedLicenseCategories(deprecatedConfig.approvedLicenseCategories); configuration.removeApprovedLicenseCategories(deprecatedConfig.removedLicenseCategories); @@ -443,9 +443,9 @@ public ReportConfiguration getConfiguration() { @Override public void execute() { try { - Reporter r = new Reporter(validate(getConfiguration())); - r.output(StyleSheets.PLAIN.getStyleSheet().ioSupplier(), () -> CloseShieldOutputStream.wrap(System.out)); - r.output(); + Reporter.Output output = new Reporter(validate(getConfiguration())).execute(); + output.format(StyleSheets.PLAIN.getStyleSheet().ioSupplier(), ReportConfiguration.SYSTEM_OUT.ioSupplier()); + output.format(output.getConfiguration()); } catch (BuildException e) { throw e; } catch (Exception ioex) { @@ -458,7 +458,7 @@ public void execute() { */ protected ReportConfiguration validate(final ReportConfiguration cfg) { try { - cfg.validate(s -> log(s, Project.MSG_WARN)); + cfg.validate(); } catch (ConfigurationException e) { throw new BuildException(e.getMessage(), e.getCause()); } @@ -474,7 +474,7 @@ protected ReportConfiguration validate(final ReportConfiguration cfg) { * @deprecated use <editCopyright> amd <editOverwrite> instead. */ @Deprecated - public static class AddLicenseHeaders extends EnumeratedAttribute { + public static final class AddLicenseHeaders extends EnumeratedAttribute { /** * add license headers and create *.new file */ diff --git a/apache-rat-tasks/src/test/java/org/apache/rat/anttasks/ReportTest.java b/apache-rat-tasks/src/test/java/org/apache/rat/anttasks/ReportTest.java index ad44dda17..e4ecdc6f2 100644 --- a/apache-rat-tasks/src/test/java/org/apache/rat/anttasks/ReportTest.java +++ b/apache-rat-tasks/src/test/java/org/apache/rat/anttasks/ReportTest.java @@ -198,7 +198,7 @@ public void testNoLicenseMatchers() { buildRule.executeTarget("testNoLicenseMatchers"); fail("Expected Exception"); } catch (BuildException e) { - final String expect = "at least one license"; + final String expect = "At least one license"; assertThat(e.getMessage()).describedAs("Expected " + expect).contains(expect); } } diff --git a/apache-rat-tasks/src/test/resources/antunit/report-bad-configurations.xml b/apache-rat-tasks/src/test/resources/antunit/report-bad-configurations.xml index 56d1e8d7a..bf43ff943 100644 --- a/apache-rat-tasks/src/test/resources/antunit/report-bad-configurations.xml +++ b/apache-rat-tasks/src/test/resources/antunit/report-bad-configurations.xml @@ -31,7 +31,7 @@ - + @@ -50,7 +50,7 @@ - + diff --git a/apache-rat-tasks/src/test/resources/antunit/report-junit.xml b/apache-rat-tasks/src/test/resources/antunit/report-junit.xml index 81827d40f..3b64f5356 100644 --- a/apache-rat-tasks/src/test/resources/antunit/report-junit.xml +++ b/apache-rat-tasks/src/test/resources/antunit/report-junit.xml @@ -226,7 +226,7 @@ public class InlineMatcher extends AbstractHeaderMatcher { - + @@ -235,7 +235,7 @@ public class InlineMatcher extends AbstractHeaderMatcher { - + diff --git a/apache-rat-tasks/src/test/resources/antunit/stylesheet.xslt b/apache-rat-tasks/src/test/resources/antunit/stylesheet.xslt new file mode 100644 index 000000000..008412298 --- /dev/null +++ b/apache-rat-tasks/src/test/resources/antunit/stylesheet.xslt @@ -0,0 +1,2 @@ +The text to match +more text to match \ No newline at end of file diff --git a/apache-rat-tools/src/main/java/org/apache/rat/tools/xsd/XsdGenerator.java b/apache-rat-tools/src/main/java/org/apache/rat/tools/xsd/XsdGenerator.java index eafcc4e4b..025109290 100644 --- a/apache-rat-tools/src/main/java/org/apache/rat/tools/xsd/XsdGenerator.java +++ b/apache-rat-tools/src/main/java/org/apache/rat/tools/xsd/XsdGenerator.java @@ -177,11 +177,10 @@ private void writeMatchers() throws IOException { } private void writeMatcherElements() throws IOException { - MatcherBuilderTracker tracker = MatcherBuilderTracker.instance(); writer.open(Type.ELEMENT, "name", XMLConfig.MATCHER, "abstract", "true").close(Type.ELEMENT); // matchers - for (Class> clazz : tracker.getClasses()) { + for (Class> clazz : MatcherBuilderTracker.instance().getClasses()) { Description desc = DescriptionBuilder.buildMap(clazz); if (desc != null) { boolean hasResourceAttr = false; diff --git a/apache-rat/pom.xml b/apache-rat/pom.xml index f915d7b05..8b4ea0f61 100644 --- a/apache-rat/pom.xml +++ b/apache-rat/pom.xml @@ -48,6 +48,13 @@ + + com.github.spotbugs + spotbugs-maven-plugin + + spotbugs-ignore.xml + + org.apache.maven.plugins maven-source-plugin diff --git a/apache-rat/spotbugs-ignore.xml b/apache-rat/spotbugs-ignore.xml index 085293a2b..b0a2af2a3 100644 --- a/apache-rat/spotbugs-ignore.xml +++ b/apache-rat/spotbugs-ignore.xml @@ -24,6 +24,10 @@ + + + q + diff --git a/pom.xml b/pom.xml index cc2dc1581..76d9d0ec8 100644 --- a/pom.xml +++ b/pom.xml @@ -61,6 +61,7 @@ agnostic home for software distribution comprehension and audit tools. RAT 2.4.1 3.1 + 2.12.0 3.5.1 3.15.2 @@ -268,6 +269,18 @@ agnostic home for software distribution comprehension and audit tools. 1.3.1 test + + org.xmlunit + xmlunit-assertj3 + ${xmlunit.version} + test + + + org.xmlunit + xmlunit-core + ${xmlunit.version} + test + diff --git a/src/site/markdown/development/ui_implementation.md b/src/site/markdown/development/ui_implementation.md index c70733ab3..cf6a35f69 100644 --- a/src/site/markdown/development/ui_implementation.md +++ b/src/site/markdown/development/ui_implementation.md @@ -20,6 +20,12 @@ The RAT architecture supports multiple UIs. By default, RAT provides a command line implementation (CLI) as well as implementations for the Ant and Maven build system. The source code for those implementation provide a good roadmap for implementing any new UI. +Our UI strategy is for core to provide basic functionality to process an Apache Commons CLI definition and arguments. UIs will process input as per their style and standards, translate those input into RAT UI options and arguments. These are used to create a `ReportConfiguration`. The `ReportConfiguration` is the constructor argument for a RAT `Reporter` which is executed and the resulting `Reporter.Output` used to update the UI as necessary. + +In addition, RAT provides a collection of `TestData` objects which define tests that the UI must pass. Each `TestData` provides a unique name for the test, the command line that should be created from the UI, a funstion to create the test directory and contents setup. and a validator to validate the results. + +Implementors are expected to create a directory, call the setup to populate it with the test data, process the commandLine to create a UI specific equivalent, execute the `Reporter` using a configuration generated by the UI and then verify the `Reporter.Output` using the `TestData` validator. + ### CLI first RAT is designed as a CLI first architecture. This means that every new functionality is introduced as a CLI option first. All tests cases are built and the implementation verified before it is released. Additional native UIs are build on top of the CLI implementation.
NOTE: should be set before licenses or license families are added.
+ Documents with unapproved licenses will start with a + The first character on the next line identifies the document type. +
Generated at by + +