You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@creadur.apache.org by cl...@apache.org on 2026/06/25 07:18:29 UTC

(creadur-rat) branch master updated: RAT-559: fix DocumentName issues in archives/on Windows (#674)

This is an automated email from the ASF dual-hosted git repository.

Claudenw pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/creadur-rat.git


The following commit(s) were added to refs/heads/master by this push:
     new d8ba8f25 RAT-559: fix DocumentName issues in archives/on Windows (#674)
d8ba8f25 is described below

commit d8ba8f253129dca800635b6485e1bcec58ff01f4
Author: Claude Warren <cl...@apache.org>
AuthorDate: Thu Jun 25 09:18:24 2026 +0200

    RAT-559: fix DocumentName issues in archives/on Windows (#674)
    
    * fix DocumentName issues
    
    * Fixed several parsing errors in DocumentName.Builder
    Add testing
    Expanded testing to include multiple file system types
    Fix recursion issue in DocumentName equals and compareTo
    Additional migration to assertj
    Changed File options to return DocumentName instances.
    Cleaned up Sonar review issues
    
    * RAT-559: Add changelog and fix Sonar issue
    
    ---------
    
    Co-authored-by: P. Ottlinger <po...@apache.org>
---
 .../main/java/org/apache/rat/commandline/Arg.java  |  48 +--
 .../org/apache/rat/commandline/Converters.java     |  39 +-
 .../org/apache/rat/document/ArchiveEntryName.java  |  34 +-
 .../java/org/apache/rat/document/DocumentName.java | 238 +++++++++---
 .../java/org/apache/rat/OptionCollectionTest.java  |  20 +-
 .../org/apache/rat/ReporterOptionsProvider.java    | 228 +++++------
 .../src/test/java/org/apache/rat/ReporterTest.java |   5 +-
 .../java/org/apache/rat/commandline/ArgTests.java  |  28 +-
 .../apache/rat/commandline/FileConverterTest.java  |  85 ++++
 .../config/exclusion/ExclusionProcessorTest.java   |  45 +--
 .../rat/document/DocumentNameBuilderTest.java      | 320 +++++++++------
 .../rat/document/DocumentNameMatcherTest.java      |  35 +-
 .../org/apache/rat/document/DocumentNameTest.java  | 429 ++++++++++-----------
 .../java/org/apache/rat/document/FSInfoTest.java   | 114 +++++-
 .../test/AbstractConfigurationOptionsProvider.java | 284 +++++++-------
 .../documentation/options/AntOptionCollection.java |   4 +-
 src/changes/changes.xml                            |   3 +
 17 files changed, 1174 insertions(+), 785 deletions(-)

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 069009aa..314e9c27 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
@@ -132,13 +132,13 @@ public enum Arg {
             .addOption(Option.builder().longOpt("config").hasArgs().argName("File")
                     .desc("File names for system configuration.")
                     .converter(Converters.FILE_CONVERTER)
-                    .type(File.class)
+                    .type(DocumentName.class)
                     .build())
             .addOption(Option.builder().longOpt("licenses").hasArgs().argName("File")
                     .desc("File names for system configuration.")
                     .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--config")).get())
                     .converter(Converters.FILE_CONVERTER)
-                    .type(File.class)
+                    .type(DocumentName.class)
                     .build()),
             Arg::doNotExecute
     ),
@@ -177,7 +177,7 @@ public enum Arg {
     LICENSES_APPROVED_FILE(new OptionGroup().addOption(Option.builder().longOpt("licenses-approved-file").hasArg().argName("File")
             .desc("Name of file containing comma separated lists of approved License IDs.")
             .converter(Converters.FILE_CONVERTER)
-            .type(File.class)
+            .type(DocumentName.class)
             .build()),
             (context, selected) ->
                     context.getConfiguration().addApprovedLicenseIds(processArrayFile(context, selected))),
@@ -198,7 +198,7 @@ public enum Arg {
     FAMILIES_APPROVED_FILE(new OptionGroup().addOption(Option.builder().longOpt("license-families-approved-file").hasArg().argName("File")
             .desc("Name of file containing comma separated lists of approved family IDs.")
             .converter(Converters.FILE_CONVERTER)
-            .type(File.class)
+            .type(DocumentName.class)
             .build()),
             (context, selected) -> context.getConfiguration().addApprovedLicenseCategories(processArrayFile(context, selected))
     ),
@@ -247,7 +247,7 @@ public enum Arg {
             .desc("Name of file containing comma separated lists of denied license IDs. " +
                     "These license families will be removed from the list of approved licenses. " +
                     "Once license families are removed they can not be added back.")
-            .type(File.class)
+            .type(DocumentName.class)
             .converter(Converters.FILE_CONVERTER)
             .build()),
             (context, selected) -> context.getConfiguration().removeApprovedLicenseCategories(processArrayFile(context, selected))),
@@ -294,12 +294,12 @@ public enum Arg {
                             "File names that do not start with '/' are relative to the directory where the " +
                             "argument is located.")
                     .converter(Converters.FILE_CONVERTER)
-                    .type(File.class)
+                    .type(DocumentName.class)
                     .build()),
             (context, selected) -> {
-                File[] files = getParsedOptionValues(selected, context.getCommandLine());
-                for (File f : files) {
-                    context.getConfiguration().addSource(f);
+                DocumentName[] documentNames = getParsedOptionValues(selected, context.getCommandLine());
+                for (DocumentName documentName : documentNames) {
+                    context.getConfiguration().addSource(documentName.asFile());
                 }
             }),
 
@@ -340,9 +340,9 @@ public enum Arg {
                     .build()),
             (context, selected) -> {
                 try {
-                    File excludeFileName = context.getCommandLine().getParsedOptionValue(selected);
+                    DocumentName excludeFileName = context.getCommandLine().getParsedOptionValue(selected);
                     if (excludeFileName != null) {
-                        context.getConfiguration().addExcludedPatterns(ExclusionUtils.asIterable(excludeFileName, "#"));
+                        context.getConfiguration().addExcludedPatterns(ExclusionUtils.asIterable(excludeFileName.asFile(), "#"));
                     }
                 } catch (Exception e) {
                     throw ConfigurationException.from(e);
@@ -422,9 +422,9 @@ public enum Arg {
                     .build()),
             (context, selected) -> {
                 try {
-                    File includeFileName = context.getCommandLine().getParsedOptionValue(selected);
+                    DocumentName includeFileName = context.getCommandLine().getParsedOptionValue(selected);
                     if (includeFileName != null) {
-                        context.getConfiguration().addIncludedPatterns(ExclusionUtils.asIterable(includeFileName, "#"));
+                        context.getConfiguration().addIncludedPatterns(ExclusionUtils.asIterable(includeFileName.asFile(), "#"));
                     }
                 } catch (Exception e) {
                     throw ConfigurationException.from(e);
@@ -605,22 +605,23 @@ public enum Arg {
             .addOption(Option.builder().option("o").longOpt("out").hasArg().argName("File")
                     .desc("Define the output file where to write a report to.")
                     .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--output-file")).get())
-                    .type(File.class)
+                    .type(DocumentName.class)
                     .converter(Converters.FILE_CONVERTER)
                     .build())
             .addOption(Option.builder().longOpt("output-file").hasArg().argName("File")
                     .desc("Define the output file where to write a report to.")
-                    .type(File.class)
+                    .type(DocumentName.class)
                     .converter(Converters.FILE_CONVERTER)
                     .build()),
             (context, selected) -> {
                 try {
-                    File file = context.getCommandLine().getParsedOptionValue(selected);
-                    File parent = file.getParentFile();
+                    DocumentName documentName = context.getCommandLine().getParsedOptionValue(selected);
+                    File document = documentName.asFile();
+                    File parent = document.getParentFile();
                     if (!parent.mkdirs() && !parent.isDirectory()) {
-                        DefaultLog.getInstance().error("Could not create report parent directory " + file);
+                        DefaultLog.getInstance().error("Could not create report parent directory " + documentName);
                     }
-                    context.getConfiguration().setOut(file);
+                    context.getConfiguration().setOut(document);
                 } catch (ParseException e) {
                     // we write to system out by default.
                     context.logParseException(e, selected, "System.out");
@@ -821,7 +822,8 @@ public enum Arg {
      * @return Option as a file.
      */
     private static File commandLineFile(final ArgumentContext context, final Option selected) throws ParseException {
-        return context.getCommandLine().getParsedOptionValue(selected);
+        DocumentName documentName = context.getCommandLine().getParsedOptionValue(selected);
+        return documentName.asFile();
     }
 
     /**
@@ -859,9 +861,9 @@ public enum Arg {
 
         optionCollection.getSelected(CONFIGURATION).ifPresent(
                 selected -> {
-                    File[] files = getParsedOptionValues(selected, context.getCommandLine());
-                    for (File file : files) {
-                        defaultBuilder.add(file);
+                    DocumentName[] documentNames = getParsedOptionValues(selected, context.getCommandLine());
+                    for (DocumentName documentName : documentNames) {
+                        defaultBuilder.add(documentName.asFile());
                     }
                 });
         optionCollection.getSelected(CONFIGURATION_NO_DEFAULTS).ifPresent(selected -> {
diff --git a/apache-rat-core/src/main/java/org/apache/rat/commandline/Converters.java b/apache-rat-core/src/main/java/org/apache/rat/commandline/Converters.java
index 15836663..15582168 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/commandline/Converters.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/commandline/Converters.java
@@ -18,9 +18,8 @@
  */
 package org.apache.rat.commandline;
 
-import java.io.File;
-import java.io.IOException;
 import java.util.Arrays;
+import java.util.Optional;
 
 import org.apache.commons.cli.Converter;
 import org.apache.commons.lang3.tuple.Pair;
@@ -73,14 +72,15 @@ public final class Converters {
     /**
      * A converter that can handle relative or absolute files.
      */
-    public static final class FileConverter implements Converter<File, NullPointerException> {
+    public static final class FileConverter implements Converter<DocumentName, NullPointerException> {
         /** The working directory to resolve relative files against. */
         private DocumentName workingDirectory;
 
         /**
          * The constructor.
+         * visible for testing
          */
-        private FileConverter() {
+        FileConverter() {
             // private construction only.
         }
 
@@ -95,24 +95,27 @@ public final class Converters {
         /**
          * Applies the conversion function to the specified file name.
          * @param fileName the file name to create a file from.
-         * @return a File.
+         * @return the DocumentName
          * @throws NullPointerException if {@code fileName} is null.
          */
-        public File apply(final String fileName) throws NullPointerException {
-            File file = new File(fileName);
-            // is this a relative file?
-            if (!fileName.startsWith(File.separator)) {
-                // check for a root provided (e.g. C:\\)"
-                if (!DocumentName.FSInfo.getDefault().rootFor(fileName).isPresent()) {
-                    // no root, resolve against workingDirectory
-                    file = new File(workingDirectory.resolve(fileName).getName()).getAbsoluteFile();
+        public DocumentName apply(final String fileName) throws NullPointerException {
+            DocumentName.FSInfo fsInfo = workingDirectory.fsInfo();
+            DocumentName.Builder builder = DocumentName.builder(fsInfo);
+            String normalizedFileName = fsInfo.normalize(fileName.trim());
+
+            Optional<String> root = fsInfo.rootFor(normalizedFileName);
+            builder.setRoot(root.orElse(workingDirectory.getRoot()));
+            if (fsInfo.startsWithRootOrSeparator(normalizedFileName)) {
+                if (normalizedFileName.startsWith(workingDirectory.getName()) ||
+                        normalizedFileName.startsWith(workingDirectory.getName().substring(workingDirectory.getRoot().length()))) {
+                    builder.setBaseName(workingDirectory.getBaseDocumentName());
+                } else {
+                    builder.setBaseName(fsInfo.dirSeparator());
                 }
+            } else {
+                builder.setBaseName(workingDirectory);
             }
-            try {
-                return file.getCanonicalFile();
-            } catch (IOException e) {
-                return file.getAbsoluteFile();
-            }
+            return  builder.setName(normalizedFileName).build();
         }
     }
 }
diff --git a/apache-rat-core/src/main/java/org/apache/rat/document/ArchiveEntryName.java b/apache-rat-core/src/main/java/org/apache/rat/document/ArchiveEntryName.java
index 13b4668e..443d4094 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/document/ArchiveEntryName.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/document/ArchiveEntryName.java
@@ -23,18 +23,33 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Collections;
 
+/**
+ * The DocumentName for an ArchiveEntry.
+ */
 public class ArchiveEntryName extends DocumentName {
     /** The name of the document that contains this entry. */
     private final DocumentName archiveFileName;
 
+    /**
+     * Sets the builder so that a proper DocumentName is constructed for an archive entry.
+     * @param archiveFileName the archive file DocumentName
+     * @param archiveEntryName the entry name
+     * @return the DocumentName.Builder for the archive entry.
+     */
     private static DocumentName.Builder prepareBuilder(final DocumentName archiveFileName, final String archiveEntryName) {
-        String root = archiveFileName.getName() + "#";
+        String root = archiveFileName.getName() + "#/";
         FSInfo fsInfo = new FSInfo("archiveEntry", "/", true, Collections.singletonList(root));
         return DocumentName.builder(fsInfo)
                 .setRoot(root)
-                .setBaseName(root + "/")
+                .setBaseName("/")
                 .setName(archiveEntryName);
     }
+
+    /**
+     * Constucts an archive file name from an archive file document name and an entry name.
+     * @param archiveFileName the archive file document name.
+     * @param archiveEntryName the archive entry name.
+     */
     public ArchiveEntryName(final DocumentName archiveFileName, final String archiveEntryName) {
         super(prepareBuilder(archiveFileName, archiveEntryName));
         this.archiveFileName = archiveFileName;
@@ -60,25 +75,10 @@ public class ArchiveEntryName extends DocumentName {
         return archiveFileName.getName() + "#";
     }
 
-    @Override
-    boolean startsWithRootOrSeparator(final String candidate, final String root, final String separator) {
-        return super.startsWithRootOrSeparator(candidate, root, separator);
-    }
-
     @Override
     public String localized(final String dirSeparator) {
         String superLocal = super.localized(dirSeparator);
         superLocal = superLocal.substring(superLocal.lastIndexOf("#") + 1);
         return archiveFileName.localized(dirSeparator) + "#" + superLocal;
     }
-
-    @Override
-    public boolean equals(final Object other) {
-        return super.equals(other);
-    }
-
-    @Override
-    public int hashCode() {
-        return super.hashCode();
-    }
 }
diff --git a/apache-rat-core/src/main/java/org/apache/rat/document/DocumentName.java b/apache-rat-core/src/main/java/org/apache/rat/document/DocumentName.java
index c2c0510e..c6cc55f2 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/document/DocumentName.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/document/DocumentName.java
@@ -27,17 +27,15 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.builder.CompareToBuilder;
-import org.apache.commons.lang3.builder.EqualsBuilder;
-import org.apache.commons.lang3.builder.HashCodeBuilder;
 import org.apache.commons.lang3.tuple.ImmutablePair;
 import org.apache.commons.lang3.tuple.Pair;
 
@@ -45,8 +43,8 @@ import org.apache.commons.lang3.tuple.Pair;
  * The name for a document. The {@code DocumentName} is an immutable structure that handles all the intricacies of file
  * naming on various operating systems. DocumentNames have several components:
  * <ul>
- *     <li>{@code root} - where in the file system the name starts (e.g C: on windows). May be empty but not null.</li>
- *     <li>{@code dirSeparator} - the separator between name segments (e.g. "\\" on windows, "/" on linux). May not be
+ *     <li>{@code root} - where in the file system the name starts (e.g C:\ on Microsoft Windows). May be empty but not null.</li>
+ *     <li>{@code dirSeparator} - the separator between name segments (e.g. "\" on Microsoft Windows, "/" on linux). May not be
  *     empty or null.</li>
  *     <li>{@code name} - the name of the file relative to the {@code root}. May not be null. Does NOT begin with a {@code dirSeparator}</li>
  *     <li>{@code baseName} - the name of a directory or file from which this file is reported. A DocumentName with a
@@ -65,7 +63,7 @@ public class DocumentName implements Comparable<DocumentName> {
     private final DocumentName baseName;
     /** The file system info for this document. */
     private final FSInfo fsInfo;
-    /** The root for the DocumentName. May be empty but not null. */
+    /** The root for the DocumentName. May be empty but not null. Must be one of the roots in fsInfo. */
     private final String root;
 
     /**
@@ -126,7 +124,7 @@ public class DocumentName implements Comparable<DocumentName> {
     }
 
     /**
-     * Creates a file from the document name.
+     * Creates a file from the fully qualified document name.
      * @return a new File object.
      */
     public File asFile() {
@@ -134,7 +132,8 @@ public class DocumentName implements Comparable<DocumentName> {
     }
 
     /**
-     * Creates a path from the document name.
+     * Creates a path from the document name. This method uses the fully qualified name without the root.
+     * this results in a relative file name from the root.
      * @return a new Path object.
      */
     public Path asPath() {
@@ -144,9 +143,23 @@ public class DocumentName implements Comparable<DocumentName> {
     /**
      * Creates a new DocumentName by adding the child to the current name.
      * Resulting documentName will have the same base name.
+     * Directory separator is normalized to the directory separator for this file system.
+     * If the child string:
+     * <dl>
+     *     <dt>Is blank</dt>
+     *     <dd>This DocumentName is returned.</dd>
+     *     <dt>Starts with the file system root</dt>
+     *     <dd>The root must match the root of this DocumentName and the directory structure
+     *     must start with the directory structure of the basename for this DocumentName.</dd>
+     *     <dt>Starts with the directory separator character<dt>
+     *     <dd>Result will be a tree starting at the directory specified by the basename.</dd>
+     *     <dt>Does not start with a directory separator character</dt>
+     *     <dd>Result will be a tree starting at the directory specified by this DocumentName</dd>
+     * </dl>
      * @param child the child to add (must use directory separator from this document name).
      * @return the new document name with the same {@link #baseName}, directory sensitivity and case sensitivity as
      * this one.
+     * @throws IllegalArgumentException if the child specifies a different root from this document name.
      */
     public DocumentName resolve(final String child) {
         if (StringUtils.isBlank(child)) {
@@ -156,8 +169,25 @@ public class DocumentName implements Comparable<DocumentName> {
         String pattern = separator.equals("/") ? child.replace('\\', '/') :
                 child.replace('/', '\\');
 
+        Optional<String> expectedRoot = fsInfo.rootFor(child);
+        if (expectedRoot.isPresent()) {
+            if (!expectedRoot.get().equals(getRoot())) {
+                throw new IllegalArgumentException(String.format("%s does not start with %s", pattern, getName()));
+            }
+            if (!getRoot().equals(separator)) {
+                // we have something like C:\ as the root so convert the pattern to start with the separator.
+                pattern = separator + pattern.substring(getRoot().length());
+                if (pattern.startsWith(baseName.name)) {
+                    pattern = pattern.substring(baseName.name.length());
+                }
+            }
+        }
+
+        // Patterns with separators either start with the name of this document plus a relative
+        // name, or are just directory off the baseName. In either case the name is correct.
+        // So just handle the relative case.
         if (!pattern.startsWith(separator)) {
-             pattern = name + separator + pattern;
+            pattern = name + separator + pattern;
         }
 
         return new Builder(this).setName(fsInfo.normalize(pattern)).build();
@@ -168,7 +198,15 @@ public class DocumentName implements Comparable<DocumentName> {
      * @return the fully qualified name of the document.
      */
     public String getName() {
-        return root + fsInfo.dirSeparator() + name;
+        return root + name;
+    }
+
+    /**
+     * Gets the path of the document. This is the fully qualified name without the root but starting with a path separator.
+     * @return the path of the document.
+     */
+    public String getPath() {
+        return getDirectorySeparator() + name;
     }
 
     /**
@@ -203,6 +241,14 @@ public class DocumentName implements Comparable<DocumentName> {
         return fsInfo.dirSeparator();
     }
 
+    /**
+     * Returns the FSInfo for this document name.
+     * @return the FSInfo for this document name.
+     */
+    public FSInfo fsInfo() {
+        return fsInfo;
+    }
+
     /**
      * Determines if the candidate starts with the root or separator strings.
      * @param candidate the candidate to check. If blank method will return {@code false}.
@@ -210,7 +256,7 @@ public class DocumentName implements Comparable<DocumentName> {
      * @param separator the separator to check. If blank the check is skipped.
      * @return true if either the root or separator check returned {@code true}.
      */
-    boolean startsWithRootOrSeparator(final String candidate, final String root, final String separator) {
+    static boolean startsWithRootOrSeparator(final String candidate, final String root, final String separator) {
         if (StringUtils.isBlank(candidate)) {
             return false;
         }
@@ -287,19 +333,30 @@ public class DocumentName implements Comparable<DocumentName> {
 
     @Override
     public int compareTo(final DocumentName other) {
-        return CompareToBuilder.reflectionCompare(this, other);
+        return new CompareToBuilder()
+                .append(this.root, other.root)
+                .append(this.getBaseName(), other.getBaseName())
+                .append(this.getName(), other.getName()).build();
     }
 
     @Override
-    public boolean equals(final Object other) {
-        return EqualsBuilder.reflectionEquals(this, other);
+    public final boolean equals(final Object other) {
+        if (other instanceof DocumentName otherDocumentName) {
+            return compareTo(otherDocumentName) == 0;
+        }
+        return false;
     }
 
     @Override
-    public int hashCode() {
-        return HashCodeBuilder.reflectionHashCode(this);
+    public final int hashCode() {
+        return getName().hashCode();
     }
 
+    /**
+     * The File System Info Data for a DocumentName.
+     * Use to preserve data across DocumentNames without having to
+     * reconstruct the data for each DocumentName.
+     */
     private static final class FSInfoData {
         /** The case sensitivity flag */
         private final boolean isCaseSensitive;
@@ -468,6 +525,14 @@ public class DocumentName implements Comparable<DocumentName> {
             return Optional.empty();
         }
 
+        /**
+         * Gets the array of roots for this file system.
+         * @return an array of roots for this file system.
+         */
+        public String[] roots() {
+            return data.roots.toArray(new String[0]);
+        }
+
         /**
          * Tokenizes the string based on the directory separator of this DocumentName.
          * @param source the source to tokenize.
@@ -483,38 +548,97 @@ public class DocumentName implements Comparable<DocumentName> {
          * @return the normalized file name.
          */
         public String normalize(final String pattern) {
-            if (StringUtils.isBlank(pattern)) {
+            if (StringUtils.isBlank(pattern) || pattern.trim().equals(".")) {
                 return "";
             }
-            List<String> parts = new ArrayList<>(Arrays.asList(tokenize(pattern)));
-            for (int i = 0; i < parts.size(); i++) {
+            String adjustedPattern = dirSeparator().equals("/") ? pattern.replace("\\", "/") : pattern.replace("/", "\\");
+            if (adjustedPattern.trim().equals(dirSeparator())) {
+                return adjustedPattern;
+            }
+            List<String> parts = new ArrayList<>(Arrays.asList(tokenize(adjustedPattern)));
+            int i = 0;
+            while (i < parts.size()) {
                 String part = parts.get(i);
                 if (part.equals("..")) {
                     if (i == 0) {
                         throw new IllegalStateException("Unable to create path before root");
                     }
-                    parts.set(i - 1, null);
-                    parts.set(i, null);
+                    parts.remove(i);
+                    parts.remove(i - 1);
+                    i--;
                 } else if (part.equals(".")) {
-                    parts.set(i, null);
+                    parts.remove(i);
+                } else {
+                    i++;
+                }
+            }
+            if (parts.isEmpty()) {
+                throw new IllegalStateException("Unable to create path before root");
+            }
+            return String.join(dirSeparator(), parts);
+        }
+
+        /**
+         * Creates a path separated by the directory separator.
+         * Starting with an empty string will cause the directory separator to appear at the beginning.
+         * @param segments the segments that make up the path.
+         * @return the path string.
+         */
+        public String mkPath(final String... segments) {
+            return String.join(dirSeparator(), segments);
+        }
+
+        /**
+         * Determines if the candidate string starts with a root or directory separator as defined in this
+         * FSInfo.
+         * @param candidate the candidate string to test.
+         * @return {@code true} if the candidate starts with a root or a directory separator.
+         */
+        public boolean startsWithRootOrSeparator(final String candidate) {
+            if (candidate == null) {
+                return false;
+            }
+            String target = candidate.trim();
+            if (StringUtils.isBlank(target)) {
+                return false;
+            }
+            for (String root : roots()) {
+                if (target.startsWith(root)) {
+                    return true;
                 }
             }
-            return parts.stream().filter(Objects::nonNull).collect(Collectors.joining(dirSeparator()));
+            return target.startsWith(dirSeparator());
+        }
+
+        private int compareData(final DocumentName.FSInfoData otherData) {
+            int result = Boolean.compare(this.data.isCaseSensitive, otherData.isCaseSensitive);
+            if (result == 0) {
+                result = this.data.separator.compareTo(otherData.separator);
+                if (result == 0) {
+                    if (new HashSet<>(this.data.roots).containsAll(otherData.roots)) {
+                        result = new HashSet<>(otherData.roots).containsAll(this.data.roots) ? 0 : 1;
+                    } else {
+                        result = -1;
+                    }
+                }
+            }
+            return result;
         }
 
         @Override
         public int compareTo(final FSInfo other) {
-            return CompareToBuilder.reflectionCompare(this, other);
+            int result = this.name.compareToIgnoreCase(other.name);
+            return result == 0 ? compareData(other.data) : result;
         }
 
         @Override
         public boolean equals(final Object other) {
-            return EqualsBuilder.reflectionEquals(this, other);
+            return other instanceof FSInfo oth && this.compareTo(oth) == 0;
         }
 
         @Override
         public int hashCode() {
-            return HashCodeBuilder.reflectionHashCode(this);
+            return name.hashCode();
         }
     }
 
@@ -538,7 +662,7 @@ public class DocumentName implements Comparable<DocumentName> {
          */
         private Builder(final FSInfo fsInfo) {
             this.fsInfo = fsInfo;
-            root = "";
+            this.root = "";
         }
 
         /**
@@ -596,6 +720,16 @@ public class DocumentName implements Comparable<DocumentName> {
             }
             if (!sameNameFlag) {
                 Objects.requireNonNull(baseName, "Basename must not be null");
+                if (this.root.isBlank()) {
+                    this.root = this.baseName.getRoot();
+                }
+            }
+            if (this.root.isBlank()) {
+                this.root = this.fsInfo.roots()[0];
+            } else {
+                if (!List.of(this.fsInfo.roots()).contains(this.root)) {
+                    throw new IllegalArgumentException(String.format("'%s' is not a valid root for %s", this.root, this.fsInfo));
+                }
             }
         }
 
@@ -622,15 +756,9 @@ public class DocumentName implements Comparable<DocumentName> {
         public Builder setName(final String name) {
             Pair<String, String> pair = splitRoot(StringUtils.defaultIfBlank(name, ""));
             if (this.root.isEmpty()) {
-                this.root = pair.getLeft();
+                setRoot(pair.getLeft());
             }
             this.name = fsInfo.normalize(pair.getRight());
-            if (this.baseName != null && !baseName.name.isEmpty()) {
-                if (!this.name.startsWith(baseName.name)) {
-                    this.name = this.name.isEmpty() ? baseName.name :
-                            baseName.name + fsInfo.dirSeparator() + this.name;
-                }
-            }
             return this;
         }
 
@@ -644,19 +772,11 @@ public class DocumentName implements Comparable<DocumentName> {
          */
         Pair<String, String> splitRoot(final String name) {
             String workingName = name;
-            Optional<String> maybeRoot = fsInfo.rootFor(name);
-            String root = maybeRoot.orElse("");
-            if (!root.isEmpty()) {
-                if (workingName.startsWith(root)) {
-                    workingName = workingName.substring(root.length());
-                    if (!workingName.startsWith(fsInfo.dirSeparator())) {
-                        if (root.endsWith(fsInfo.dirSeparator())) {
-                            root = root.substring(0, root.length() - fsInfo.dirSeparator().length());
-                        }
-                    }
-                }
+            String workingRoot = fsInfo.rootFor(name).orElse("");
+            if (!workingRoot.isEmpty() && workingName.startsWith(workingRoot)) {
+                workingName = workingName.substring(workingRoot.length());
             }
-            return ImmutablePair.of(root, workingName);
+            return ImmutablePair.of(workingRoot, workingName);
         }
 
         /**
@@ -745,12 +865,38 @@ public class DocumentName implements Comparable<DocumentName> {
             return this;
         }
 
+        // only called if basName is not null
+        private void verifyBaseName() {
+            if (!this.name.startsWith(baseName.name)) {
+                this.name = this.name.isEmpty() ? baseName.name :
+                        baseName.name + fsInfo.dirSeparator() + this.name;
+            }
+            if (!this.baseName.getRoot().equals(root)) {
+                Builder builder = new Builder(baseName).setRoot(root);
+                if (baseName.baseName != null && baseName.baseName != baseName) {
+                    builder.setBaseName(baseName.baseName);
+                } else {
+                    builder.baseName = null;
+                    builder.sameNameFlag = true;
+                }
+                this.baseName = builder.build();
+            }
+
+        }
+
         /**
          * Build a DocumentName from this builder.
          * @return A new DocumentName.
          */
         public DocumentName build() {
             verify();
+            if (this.baseName != null) {
+                verifyBaseName();
+            } else {
+                if (this.name.startsWith(root)) {
+                    this.name = this.name.substring(root.length());
+                }
+            }
             return new DocumentName(this);
         }
     }
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 64c11257..238bc69c 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
@@ -67,6 +67,7 @@ import static java.lang.String.format;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Fail.fail;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 
 public class OptionCollectionTest {
     @TempDir(cleanup = CleanupMode.NEVER)
@@ -135,6 +136,14 @@ public class OptionCollectionTest {
     public @interface TestFunction {
     }
 
+    /**
+     * Gets the test path for.
+     * @return the test path.
+     */
+    public static Path getTestPath() {
+        return testPath;
+    }
+
     /**
      * Process methods in a test provider.
      * Tests are detected by looking for the {@link TestFunction} annotation.
@@ -221,7 +230,8 @@ public class OptionCollectionTest {
     @ValueSource(strings = { ".", "./", "target", "./target" })
     public void getReportableTest(String fName) throws IOException {
         File base = new File(fName);
-        String expected = DocumentName.FSInfo.getDefault().normalize(base.getAbsolutePath());
+        DocumentName.FSInfo fsInfo = new DocumentName.FSInfo(testPath.getFileSystem());
+        String expected = fsInfo.normalize(base.getAbsolutePath());
         ReportConfiguration config = OptionCollection.parseCommands(testPath.toFile(), new String[]{fName}, o -> fail("Help called"), false);
         IReportable reportable = OptionCollection.getReportable(base, config);
         assertThat(reportable).as(() -> format("'%s' returned null", fName)).isNotNull();
@@ -251,7 +261,6 @@ public class OptionCollectionTest {
         assertThat(reportable).isInstanceOf(ArchiveWalker.class);
     }
 
-
     /**
      * A parameterized test for the options.
      * @param name The name of the test.
@@ -277,13 +286,12 @@ public class OptionCollectionTest {
          */
         public void helpTest() {
             String[] args = { OptionFormatter.longOpt(OptionCollection.HELP) };
-            try {
+
+            assertDoesNotThrow(() -> {
                 ReportConfiguration config = OptionCollection.parseCommands(testPath.toFile(), args, o -> helpCalled.set(true), true);
                 assertThat(config).as("Should not have config").isNull();
                 assertThat(helpCalled.get()).as("Help was not called").isTrue();
-            } catch (IOException e) {
-                fail(e.getMessage());
-            }
+            });
         }
 
         /**
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 84d21a8c..0ef6f019 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
@@ -34,7 +34,6 @@ import java.util.List;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.regex.Pattern;
 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.cli.Options;
@@ -63,11 +62,11 @@ import org.apache.rat.utils.DefaultLog;
 import org.apache.rat.utils.Log;
 import org.junit.jupiter.params.provider.ArgumentsProvider;
 import org.w3c.dom.Document;
-import org.xml.sax.SAXException;
 
 import static org.apache.rat.commandline.Arg.HELP_LICENSES;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Fail.fail;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 
 /**
  * A class to provide the Options and tests to the testOptionsUpdateConfig.
@@ -115,6 +114,13 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
         return config;
     }
 
+    private File configureRatDir(Option option) {
+        configureSourceDir(option);
+        File result = new File(sourceDir, ".rat");
+        FileUtils.mkDir(result);
+        return result;
+    }
+
     private void configureSourceDir(Option option) {
         sourceDir = new File(baseDir, OptionFormatter.getName(option));
         FileUtils.mkDir(sourceDir);
@@ -133,7 +139,6 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
         } finally {
             DefaultLog.setInstance(null);
         }
-
     }
 
     @OptionCollectionTest.TestFunction
@@ -147,7 +152,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
     }
 
     private void editLicenseTest(final Option option) {
-        try {
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
             ReportConfiguration config = generateConfig(ImmutablePair.of(option, null));
             File testFile = writeFile("NoLicense.java", "class NoLicense {}");
@@ -182,9 +187,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
                      */
                     
                     class NoLicense {}""");
-        } catch (IOException | RatException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     protected File writeFile(String name) {
@@ -201,7 +204,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
     @OptionCollectionTest.TestFunction
     private void execLicensesDeniedTest(final Option option, final String[] args) {
-        try {
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
             File illumosFile = writeFile("illumousFile.java", "The contents of this file are " +
                     "subject to the terms of the Common Development and Distribution License (the \"License\") You " +
@@ -214,9 +217,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
             ClaimStatistic claimStatistic = reporter.execute();
             ClaimValidator validator = config.getClaimValidator();
             assertThat(validator.listIssues(claimStatistic)).containsExactly("UNAPPROVED");
-        } catch (IOException | RatException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     @OptionCollectionTest.TestFunction
@@ -226,13 +227,14 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
     @OptionCollectionTest.TestFunction
     protected void licensesDeniedFileTest() {
-        File outputFile = FileUtils.writeFile(baseDir, "licensesDenied.txt", Collections.singletonList("ILLUMOS"));
-        execLicensesDeniedTest(Arg.LICENSES_DENIED_FILE.find("licenses-denied-file"),
-                new String[]{outputFile.getAbsolutePath()});
+        Option option = Arg.LICENSES_DENIED_FILE.find("licenses-denied-file");
+        File ratDir = configureRatDir(option);
+        File outputFile = FileUtils.writeFile(ratDir, "licensesDenied.txt", Collections.singletonList("ILLUMOS"));
+        execLicensesDeniedTest(option, new String[]{outputFile.getAbsolutePath()});
     }
 
     private void noDefaultsTest(final Option option) {
-        try {
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
             File testFile = writeFile("Test.java", Arrays.asList("/*\n", "SPDX-License-Identifier: Apache-2.0\n",
                     "*/\n\n", "class Test {}\n"));
@@ -249,9 +251,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
                 ClaimValidator validator = config.getClaimValidator();
                 assertThat(validator.listIssues(claimStatistic)).containsExactlyInAnyOrder("DOCUMENT_TYPES", "LICENSE_CATEGORIES", "LICENSE_NAMES", "STANDARDS");
             }
-        } catch (IOException | RatException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     @OptionCollectionTest.TestFunction
@@ -268,7 +268,8 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
     protected void counterMaxTest() {
         Option option = Arg.COUNTER_MAX.option();
         String[] arg = {null};
-        try {
+
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
             File testFile = writeFile("Test.java", Arrays.asList("/*\n", "SPDX-License-Identifier: Unapproved\n",
                     "*/\n\n", "class Test {}\n"));
@@ -285,9 +286,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
             claimStatistic = reporter.execute();
             validator = config.getClaimValidator();
             assertThat(validator.listIssues(claimStatistic)).isEmpty();
-        } catch (IOException | RatException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     @OptionCollectionTest.TestFunction
@@ -295,7 +294,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
         Option option = Arg.COUNTER_MIN.option();
         String[] arg = {null};
 
-        try {
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
             File testFile = writeFile("Test.java", Arrays.asList("/*\n", "SPDX-License-Identifier: Unapproved\n",
                     "*/\n\n", "class Test {}\n"));
@@ -312,16 +311,15 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
             claimStatistic = reporter.execute();
             validator = config.getClaimValidator();
             assertThat(validator.listIssues(claimStatistic)).isEmpty();
-        } catch (IOException | RatException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     // exclude tests
-    private void execExcludeTest(final Option option, final String[] args) {
+    private void execExcludeTest(final Option option, final String[] args, final boolean addIgnored) {
         String[] notExcluded = {"notbaz", "well._afile"};
         String[] excluded = {"some.foo", "B.bar", "justbaz"};
-        try {
+
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
             writeFile("notbaz");
             writeFile("well._afile");
@@ -333,23 +331,21 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
             Reporter reporter = new Reporter(config);
             ClaimStatistic claimStatistic = reporter.execute();
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(5);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(0);
+            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(addIgnored ? 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(3);
-        } catch (IOException | RatException e) {
-            fail(e.getMessage(), e);
-        }
+            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(addIgnored ? 4 : 3);
+        });
     }
 
     private void excludeFileTest(final Option option) {
-        configureSourceDir(option);
-        File outputFile = FileUtils.writeFile(baseDir, "exclude.txt", Arrays.asList(EXCLUDE_ARGS));
-        execExcludeTest(option, new String[]{outputFile.getAbsolutePath()});
+        File ratDir = configureRatDir(option);
+        File outputFile = FileUtils.writeFile(ratDir, "exclude.txt", Arrays.asList(EXCLUDE_ARGS));
+        execExcludeTest(option, new String[]{outputFile.getAbsolutePath()}, true);
     }
 
     @OptionCollectionTest.TestFunction
@@ -364,12 +360,12 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
     @OptionCollectionTest.TestFunction
     protected void excludeTest() {
-        execExcludeTest(Arg.EXCLUDE.find("exclude"), EXCLUDE_ARGS);
+        execExcludeTest(Arg.EXCLUDE.find("exclude"), EXCLUDE_ARGS, false);
     }
 
     @OptionCollectionTest.TestFunction
     protected void inputExcludeTest() {
-        execExcludeTest(Arg.EXCLUDE.find("input-exclude"), EXCLUDE_ARGS);
+        execExcludeTest(Arg.EXCLUDE.find("input-exclude"), EXCLUDE_ARGS, false);
     }
 
     @OptionCollectionTest.TestFunction
@@ -377,7 +373,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
         Option option = Arg.EXCLUDE_SIZE.option();
         String[] args = {"5"};
 
-        try {
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
             writeFile("Hi.txt", "Hi");
             writeFile("Hello.txt", "Hello");
@@ -395,9 +391,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
             claimStatistic = reporter.execute();
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2);
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(1);
-        } catch (IOException | RatException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     @OptionCollectionTest.TestFunction
@@ -407,7 +401,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
         // these files are excluded by default "afile~", ".#afile", "%afile%", "._afile"
         // these files are not excluded by default "afile~more", "what.#afile", "%afile%withMore", "well._afile", "build.log"
         // build.log is excluded by MAVEN.
-        try {
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
             writeFile("afile~");
             writeFile(".#afile");
@@ -430,9 +424,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
             claimStatistic = reporter.execute();
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(4);
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(5);
-        } catch (IOException | RatException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     @OptionCollectionTest.TestFunction
@@ -447,7 +439,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
                 "# some colorful directories",
                 "red/", "blue/*/"};
 
-        try {
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
 
             writeFile(".gitignore", Arrays.asList(lines));
@@ -489,17 +481,16 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
             claimStatistic = reporter.execute();
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3);
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(8);
-        } catch (IOException | RatException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     // include tests
-    private void execIncludeTest(final Option option, final String[] args) {
+    private void execIncludeTest(final Option option, final String[] args, boolean addIgnored) {
         Option excludeOption = Arg.EXCLUDE.option();
         String[] notExcluded = {"B.bar", "justbaz", "notbaz"};
         String[] excluded = {"some.foo"};
-        try {
+
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
             writeFile("notbaz");
             writeFile("some.foo");
@@ -510,7 +501,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
             Reporter reporter = new Reporter(config);
             ClaimStatistic claimStatistic = reporter.execute();
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(4);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(0);
+            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(addIgnored ? 1 : 0);
 
             // verify exclude removes most files.
             config = generateConfig(ImmutablePair.of(excludeOption, EXCLUDE_ARGS));
@@ -518,7 +509,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
             claimStatistic = reporter.execute();
             assertThat(claimStatistic.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(3);
+            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(addIgnored ? 4 : 3);
 
             // verify include pust them back
             config = generateConfig(ImmutablePair.of(option, args), ImmutablePair.of(excludeOption, EXCLUDE_ARGS));
@@ -526,15 +517,14 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
             claimStatistic = reporter.execute();
             assertThat(claimStatistic.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(1);
-        } catch (IOException | RatException e) {
-            fail(e.getMessage(), e);
-        }
+            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(addIgnored ? 2 : 1);
+        });
     }
 
     private void includeFileTest(final Option option) {
-        File outputFile = FileUtils.writeFile(baseDir, "include.txt", Arrays.asList(INCLUDE_ARGS));
-        execIncludeTest(option, new String[]{outputFile.getAbsolutePath()});
+        File ratDir = configureRatDir(option);
+        File outputFile = FileUtils.writeFile(ratDir, "include.txt", Arrays.asList(INCLUDE_ARGS));
+        execIncludeTest(option, new String[]{outputFile.getAbsolutePath()}, true);
     }
 
     @OptionCollectionTest.TestFunction
@@ -549,19 +539,20 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
     @OptionCollectionTest.TestFunction
     protected void includeTest() {
-        execIncludeTest(Arg.INCLUDE.find("include"), INCLUDE_ARGS);
+        execIncludeTest(Arg.INCLUDE.find("include"), INCLUDE_ARGS, false);
     }
 
     @OptionCollectionTest.TestFunction
     protected void inputIncludeTest() {
-        execIncludeTest(Arg.INCLUDE.find("input-include"), INCLUDE_ARGS);
+        execIncludeTest(Arg.INCLUDE.find("input-include"), INCLUDE_ARGS, false);
     }
 
     @OptionCollectionTest.TestFunction
     protected void inputIncludeStdTest() {
         Option option = Arg.INCLUDE_STD.find("input-include-std");
         String[] args = {StandardCollection.MISC.name()};
-        try {
+
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
 
             writeFile("afile~more");
@@ -587,15 +578,14 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
             claimStatistic = reporter.execute();
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(7);
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(1);
-        } catch (IOException | RatException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     @OptionCollectionTest.TestFunction
     protected void inputSourceTest() {
         Option option = Arg.SOURCE.find("input-source");
-        try {
+
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
 
             writeFile("codefile");
@@ -613,9 +603,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
             claimStatistic = reporter.execute();
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(0);
-        } catch (IOException | RatException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     private ReportConfiguration addCatzLicense(ReportConfiguration config) {
@@ -628,7 +616,8 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
     private void execLicenseFamiliesApprovedTest(final Option option, final String[] args) {
         Pair<Option, String[]> arg1 = ImmutablePair.of(option, args);
-        try {
+
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
             // write the catz licensed text file
             writeFile("catz.txt", "SPDX-License-Identifier: catz");
@@ -647,15 +636,13 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1);
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0);
-        } catch (IOException | RatException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     @OptionCollectionTest.TestFunction
     protected void licenseFamiliesApprovedFileTest() {
         Option option = Arg.FAMILIES_APPROVED_FILE.find("license-families-approved-file");
-        File outputFile = FileUtils.writeFile(baseDir, "familiesApproved.txt", Collections.singletonList("catz"));
+        File outputFile = FileUtils.writeFile(configureRatDir(option), "familiesApproved.txt", Collections.singletonList("catz"));
         execLicenseFamiliesApprovedTest(option, new String[]{outputFile.getAbsolutePath()});
     }
 
@@ -668,7 +655,8 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
     private void execLicenseFamiliesDeniedTest(final Option option, final String[] args) {
         Pair<Option, String[]> arg1 = ImmutablePair.of(option, args);
         String bsd = ILicenseFamily.makeCategory("BSD-3");
-        try {
+
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
 
             // write the catz licensed text file
@@ -687,16 +675,14 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(0);
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
-        } catch (IOException | RatException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     @OptionCollectionTest.TestFunction
     protected void licenseFamiliesDeniedFileTest() {
-        File outputFile = FileUtils.writeFile(baseDir, "familiesDenied.txt", Collections.singletonList("BSD-3"));
-        execLicenseFamiliesDeniedTest(Arg.FAMILIES_DENIED_FILE.find("license-families-denied-file"),
-                new String[]{outputFile.getAbsolutePath()});
+        Option option = Arg.FAMILIES_DENIED_FILE.find("license-families-denied-file");
+        File outputFile = FileUtils.writeFile(configureRatDir(option), "familiesDenied.txt", Collections.singletonList("BSD-3"));
+        execLicenseFamiliesDeniedTest(option, new String[]{outputFile.getAbsolutePath()});
     }
 
     @OptionCollectionTest.TestFunction
@@ -707,12 +693,11 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
 
     private void configTest(final Option option) {
-        try {
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
             String[] args = {
                     Resources.getResourceFile("OptionTools/One.xml").getAbsolutePath(),
                     Resources.getResourceFile("OptionTools/Two.xml").getAbsolutePath()};
-
             Pair<Option, String[]> arg1 = ImmutablePair.of(option, args);
 
             writeFile("bsd.txt", "SPDX-License-Identifier: BSD-3-Clause");
@@ -741,9 +726,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1);
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
 
-        } catch (IOException | RatException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     @OptionCollectionTest.TestFunction
@@ -760,7 +743,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
     protected void execLicensesApprovedTest(final Option option, String[] args) {
         Pair<Option, String[]> arg1 = ImmutablePair.of(option, args);
 
-        try {
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
 
             writeFile("gpl.txt", "SPDX-License-Identifier: GPL-1.0-only");
@@ -779,18 +762,15 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2);
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(2);
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0);
-
-
-        } catch (IOException | RatException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     @OptionCollectionTest.TestFunction
     protected void licensesApprovedFileTest() {
-        File outputFile = FileUtils.writeFile(baseDir, "licensesApproved.txt", Collections.singletonList("GPL1"));
-        execLicensesApprovedTest(Arg.LICENSES_APPROVED_FILE.find("licenses-approved-file"),
-                new String[]{outputFile.getAbsolutePath()});
+        Option option = Arg.LICENSES_APPROVED_FILE.find("licenses-approved-file");
+        File ratDir = configureRatDir(option);
+                File outputFile = FileUtils.writeFile(ratDir, "licensesApproved.txt", Collections.singletonList("GPL1"));
+        execLicensesApprovedTest(option, new String[]{outputFile.getAbsolutePath()});
     }
 
     @OptionCollectionTest.TestFunction
@@ -804,7 +784,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
         Option option = Arg.INCLUDE_STD.find("scan-hidden-directories");
         Pair<Option, String[]> arg1 = ImmutablePair.of(option, null);
 
-        try {
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
 
             writeFile("apl.txt", "SPDX-License-Identifier: Apache-2.0");
@@ -826,13 +806,11 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2);
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1);
             assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
-        } catch (IOException | RatException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     private void outTest(final Option option) {
-        try {
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
             writeFile("apl.txt", "SPDX-License-Identifier: Apache-2.0");
             File outFile = new File(sourceDir, "outexample");
@@ -849,10 +827,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
             String actualText = TextUtils.readFile(outFile);
             TextUtils.assertContainsExactly(1, "Apache License 2.0: 1 ", actualText);
             TextUtils.assertContainsExactly(1, "STANDARD: 1 ", actualText);
-
-        } catch (IOException | RatException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     @OptionCollectionTest.TestFunction
@@ -1006,7 +981,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
         XPath xPath = XPathFactory.newInstance().newXPath();
         String[] args = {null};
 
-        try {
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
             File outFile = new File(sourceDir, "out.xml");
             FileUtils.delete(outFile);
@@ -1035,10 +1010,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
                         throw new IllegalArgumentException("Unexpected filter: " + filter);
                 }
             }
-        } catch (IOException | RatException | SAXException |
-                 XPathExpressionException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     @OptionCollectionTest.TestFunction
@@ -1055,7 +1027,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
         XPath xPath = XPathFactory.newInstance().newXPath();
         String[] args = {null};
 
-        try {
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
             File outFile = new File(sourceDir, "out.xml");
             FileUtils.delete(outFile);
@@ -1084,10 +1056,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
                         throw new IllegalArgumentException("Unexpected filter: " + filter);
                 }
             }
-        } catch (IOException | RatException | SAXException |
-                 XPathExpressionException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     @OptionCollectionTest.TestFunction
@@ -1104,7 +1073,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
         XPath xPath = XPathFactory.newInstance().newXPath();
         String[] args = {null};
 
-        try {
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
             File outFile = new File(sourceDir, "out.xml");
             FileUtils.delete(outFile);
@@ -1142,10 +1111,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
                         throw new IllegalArgumentException("Unexpected processing " + proc);
                 }
             }
-        } catch (IOException | RatException | SAXException |
-                 XPathExpressionException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     @OptionCollectionTest.TestFunction
@@ -1157,7 +1123,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
         XPath xPath = XPathFactory.newInstance().newXPath();
         String[] args = {null};
 
-        try {
+        assertDoesNotThrow(() -> {
             configureSourceDir(option);
             File outFile = new File(sourceDir, "out.xml");
             ImmutablePair<Option, String[]> outputFile = ImmutablePair.of(Arg.OUTPUT_FILE.option(), new String[]{outFile.getAbsolutePath()});
@@ -1197,10 +1163,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
                         throw new IllegalArgumentException("Unexpected processing " + proc);
                 }
             }
-        } catch (IOException | RatException | SAXException |
-                 XPathExpressionException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     @OptionCollectionTest.TestFunction
@@ -1213,12 +1176,13 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
         Pair<Option, String[]> arg1 = ImmutablePair.of(option, new String[]{myCopyright});
         final boolean forced = Arg.EDIT_OVERWRITE.option().equals(extraOption);
         final boolean dryRun = Arg.DRY_RUN.option().equals(extraOption);
-        Pair<Option, String[]> extraArg = null;
-        if (forced || dryRun) {
-            extraArg = ImmutablePair.of(extraOption, null);
-        }
 
-        try {
+        assertDoesNotThrow(() -> {
+            Pair<Option, String[]> extraArg = null;
+            if (forced || dryRun) {
+                extraArg = ImmutablePair.of(extraOption, null);
+            }
+
             configureSourceDir(option);
             File javaFile = writeFile("Missing.java", Arrays.asList("/* no license */\n\n", "class Test {}\n"));
             File newJavaFile = new File(sourceDir, "Missing.java.new");
@@ -1247,9 +1211,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
                 TextUtils.assertNotContains(myCopyright, actualText);
                 assertThat(newJavaFile).exists();
             }
-        } catch (IOException | RatException e) {
-            fail(e.getMessage(), e);
-        }
+        });
     }
 
     @OptionCollectionTest.TestFunction
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 39d780c9..3d9c84e7 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
@@ -209,9 +209,10 @@ public class ReporterTest {
         expected.put("/tri.txt", mapOf("encoding", "ISO-8859-1", "mediaType", "text/plain",
                 "type", "STANDARD"));
 
-        File output = new File(tempDirectory, "testXMLOutput");
+        File output = new File(tempDirectory, ".rat/testXMLOutput");
+        output.getParentFile().mkdir();
         CommandLine commandLine = new DefaultParser().parse(OptionCollection.buildOptions(), new String[]{"--output-style", "xml", "--output-file", output.getPath(), basedir});
-        ArgumentContext ctxt = new ArgumentContext(new File("."), commandLine);
+        ArgumentContext ctxt = new ArgumentContext(tempDirectory, commandLine);
 
         ReportConfiguration config = OptionCollection.createConfiguration(ctxt);
         new Reporter(config).output();
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 1d51a5f2..742632c5 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
@@ -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  *
@@ -18,7 +18,6 @@
  */
 package org.apache.rat.commandline;
 
-import java.io.IOException;
 import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.DefaultParser;
 import org.apache.commons.cli.Options;
@@ -32,6 +31,7 @@ import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
 
 import java.io.File;
+import java.nio.file.Path;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -45,21 +45,31 @@ public class ArgTests {
 
     @ParameterizedTest(name = "{0}")
     @ValueSource(strings = { "rat.txt", "./rat.txt", "/rat.txt", "target/rat.test" })
-    public void outputFileNameNoDirectoryTest(String name) throws ParseException, IOException {
-        class OutputFileConfig extends ReportConfiguration  {
+    public void outputFileNameNoDirectoryTest(String name) throws ParseException {
+        class OutputFileConfig extends ReportConfiguration {
             private File actual = null;
+
             @Override
             public void setOut(File file) {
                 actual = file;
             }
         }
-        String fileName = name.replace("/", DocumentName.FSInfo.getDefault().dirSeparator());
-        File expected = new File(fileName);
 
-        CommandLine commandLine = createCommandLine(new String[] {"--output-file", fileName});
+        DocumentName.FSInfo fsInfo = DocumentName.FSInfo.getDefault();
+        String fileName = name.replace("/", fsInfo.dirSeparator());
+        File localFile = new File(".");
+        DocumentName localFileName = DocumentName.builder(localFile).build();
+        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(new File("."), configuration, commandLine);
+        ArgumentContext ctxt = new ArgumentContext(localFile, configuration, commandLine);
         Arg.processArgs(ctxt, CLIOptionCollection.INSTANCE);
-        assertThat(configuration.actual.getAbsolutePath()).isEqualTo(expected.getCanonicalPath());
+        if (name.equals("/rat.txt")) {
+            assertThat(fsInfo.normalize(configuration.actual.getAbsolutePath())).isEqualTo(localFileName.getRoot() + "rat.txt");
+        } else {
+            assertThat(fsInfo.normalize(configuration.actual.toString())).isEqualTo(expected);
+        }
     }
 }
diff --git a/apache-rat-core/src/test/java/org/apache/rat/commandline/FileConverterTest.java b/apache-rat-core/src/test/java/org/apache/rat/commandline/FileConverterTest.java
new file mode 100644
index 00000000..f1922638
--- /dev/null
+++ b/apache-rat-core/src/test/java/org/apache/rat/commandline/FileConverterTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.commandline;
+
+import org.apache.rat.document.DocumentName;
+import org.apache.rat.document.FSInfoTest;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class FileConverterTest {
+
+    @ParameterizedTest
+    @MethodSource("applyTestData")
+    void applyTest(DocumentName.FSInfo fsInfo, String fileName, Converters.FileConverter underTest, String expected) {
+        assertThat(underTest.apply(fileName).getName()).isEqualTo(expected);
+    }
+
+    /**
+     * Record returned from createDocumentData
+     * @param documentName the document name for the working directory.
+     * @param defaultRoot the default root for the file system
+     * @param workingBaseName the working base name.
+     * @param baseName the base name.
+     */
+    private record DocumentData(DocumentName documentName, String defaultRoot, String workingBaseName, String baseName) {
+    }
+
+    /**
+     * Creates a DocumentData from a FSInfo.
+     * @param fsInfo the FSInfo to work with.
+     * @return the DocumentData for the working directory.
+     */
+    private static DocumentData createDocumentData(final DocumentName.FSInfo fsInfo) {
+        String defaultRoot = fsInfo.roots()[0];
+        String workingBaseName = defaultRoot + "working";
+        String baseName = workingBaseName + fsInfo.dirSeparator() + "base";
+        return new DocumentData(DocumentName.builder(fsInfo).setRoot(defaultRoot).setBaseName(workingBaseName).setName("base").build(),
+                defaultRoot, workingBaseName, baseName);
+    }
+
+    static List<Arguments> applyTestData() {
+        List<Arguments> lst = new ArrayList<>();
+
+        for (DocumentName.FSInfo fsInfo : FSInfoTest.TEST_SUITE) {
+            final DocumentData workingDirectory = createDocumentData(fsInfo);
+            final Converters.FileConverter underTest = new Converters.FileConverter();
+            underTest.setWorkingDirectory(workingDirectory.documentName);
+
+            lst.add(Arguments.of(fsInfo, "/foo/bar.txt", underTest, workingDirectory.defaultRoot + String.join(fsInfo.dirSeparator(), "foo", "bar.txt")));
+            lst.add(Arguments.of(fsInfo, "\\foo\\bar.txt", underTest, workingDirectory.defaultRoot + String.join(fsInfo.dirSeparator(), "foo", "bar.txt")));
+
+            lst.add(Arguments.of(fsInfo, "foo/bar.txt", underTest, workingDirectory.baseName + String.join(fsInfo.dirSeparator(), "", "foo", "bar.txt")));
+            lst.add(Arguments.of(fsInfo, "foo\\bar.txt", underTest, workingDirectory.baseName + String.join(fsInfo.dirSeparator(), "", "foo", "bar.txt")));
+
+            if (fsInfo.equals(FSInfoTest.WINDOWS)) {
+                String root = fsInfo.roots()[1];
+                lst.add(Arguments.of(fsInfo, root + "foo/bar.txt", underTest, root + String.join(fsInfo.dirSeparator(), "foo", "bar.txt")));
+                lst.add(Arguments.of(fsInfo, root + "foo\\bar.txt", underTest, root + String.join(fsInfo.dirSeparator(), "foo", "bar.txt")));
+            }
+        }
+        return lst;
+    }
+}
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 85d8d976..19e2e154 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
@@ -30,7 +30,6 @@ import org.junit.jupiter.api.io.TempDir;
 
 import java.io.File;
 import java.io.IOException;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
@@ -44,12 +43,10 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.apache.rat.document.FSInfoTest.OSX;
 import static org.apache.rat.document.FSInfoTest.UNIX;
 import static org.apache.rat.document.FSInfoTest.WINDOWS;
+import static org.junit.jupiter.api.Assertions.fail;
 
 public class ExclusionProcessorTest {
 
-    final private static DocumentNameMatcher TRUE = DocumentNameMatcher.MATCHES_ALL;
-    final private static DocumentNameMatcher FALSE = DocumentNameMatcher.MATCHES_NONE;
-
     @TempDir
     private static Path tempDir;
 
@@ -70,10 +67,12 @@ public class ExclusionProcessorTest {
         String fn = result.localized(FileSystems.getDefault().getSeparator());
         File file = tempDir.resolve(fn.substring(1)).toFile();
         File parent = file.getParentFile();
-        if (parent.exists() && !parent.isDirectory()) {
-            parent.delete();
+        if (parent.exists() && !parent.isDirectory() && !parent.delete()) {
+            fail(() -> "Unable to delete parent: " + parent);
+        }
+        if (!parent.exists() && !parent.mkdirs()) {
+            fail((() -> "Unable to create parent: " + parent));
         }
-        parent.mkdirs();
         if (file.exists()) {
             if (file.isDirectory()) {
                 FileUtils.deleteDirectory(file);
@@ -81,7 +80,9 @@ public class ExclusionProcessorTest {
                 FileUtils.delete(file);
             }
         }
-        file.createNewFile();
+        if (!file.createNewFile()) {
+            fail(() -> "Unable to create file: " + file);
+        }
         Mockito.when(mocked.asFile()).thenReturn(file);
         return mocked;
     }
@@ -246,34 +247,6 @@ public class ExclusionProcessorTest {
         assertExclusions(basedir, "**/foo/**", expectedMap);
     }
 
-    @ParameterizedTest
-    @MethodSource("getDocumentNames")
-    void orTest(DocumentName basedir) {
-        ExclusionProcessor underTest = new ExclusionProcessor();
-        assertThat(DocumentNameMatcher.or(Arrays.asList(TRUE, FALSE)).matches(basedir)).isTrue();
-        assertThat(DocumentNameMatcher.or(Arrays.asList(FALSE, TRUE)).matches(basedir)).isTrue();
-        assertThat(DocumentNameMatcher.or(Arrays.asList(TRUE, TRUE)).matches(basedir)).isTrue();
-        assertThat(DocumentNameMatcher.or(Arrays.asList(FALSE, FALSE)).matches(basedir)).isFalse();
-    }
-
-    @ParameterizedTest
-    @MethodSource("getDocumentNames")
-    void andTest(DocumentName basedir) {
-        ExclusionProcessor underTest = new ExclusionProcessor();
-        assertThat(DocumentNameMatcher.and(TRUE, FALSE).matches(basedir)).isFalse();
-        assertThat(DocumentNameMatcher.and(FALSE, TRUE).matches(basedir)).isFalse();
-        assertThat(DocumentNameMatcher.and(TRUE, TRUE).matches(basedir)).isTrue();
-        assertThat(DocumentNameMatcher.and(FALSE, FALSE).matches(basedir)).isFalse();
-    }
-
-    @ParameterizedTest
-    @MethodSource("getDocumentNames")
-    void notTest(DocumentName basedir) {
-        ExclusionProcessor underTest = new ExclusionProcessor();
-        assertThat(DocumentNameMatcher.not(TRUE).matches(basedir)).isFalse();
-        assertThat(DocumentNameMatcher.not(FALSE).matches(basedir)).isTrue();
-    }
-
     private static Stream<Arguments> getDocumentNames() {
         List<Arguments> lst = new ArrayList<>();
 
diff --git a/apache-rat-core/src/test/java/org/apache/rat/document/DocumentNameBuilderTest.java b/apache-rat-core/src/test/java/org/apache/rat/document/DocumentNameBuilderTest.java
index 0e0f0d1f..d7a4c8b6 100644
--- a/apache-rat-core/src/test/java/org/apache/rat/document/DocumentNameBuilderTest.java
+++ b/apache-rat-core/src/test/java/org/apache/rat/document/DocumentNameBuilderTest.java
@@ -19,23 +19,40 @@
 package org.apache.rat.document;
 
 import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.stream.Stream;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.FieldSource;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.apache.rat.document.FSInfoTest.WINDOWS;
 
+/**
+ * Tests the DcoumentName.Builder class
+ */
 public class DocumentNameBuilderTest {
 
-    @ParameterizedTest(name="{0}")
-    @MethodSource("buildTestData")
-    void buildTest(String testName, DocumentName documentName, String name, String shortName, String baseName, String root,
+    private static final DocumentName.FSInfo[] TEST_SUITE = FSInfoTest.TEST_SUITE;
+
+    /**
+     * Validates the data in a document name matches expected data.
+     * @param documentName the document name to check.
+     * @param name the expected fully qualified name.
+     * @param shortName the name for the last segment of the name.
+     * @param baseName the name of the base document.
+     * @param root the root the document is in.
+     * @param directorySeparator the expected directory separator.
+     * @param isCaseSensitive the expected case sensitivity.
+     * @param localized the default localized name (e.g. path and file name from base name).
+     * @param localizedArg the localized name with directory separator set to '+'
+     */
+    void assertDocumentName(DocumentName documentName, String name, String shortName, String baseName, String root,
                    String directorySeparator, Boolean isCaseSensitive, String localized, String localizedArg) {
         assertThat(documentName.getName()).as("Invalid name").isEqualTo(name);
         assertThat(documentName.getShortName()).as("Invalid short name").isEqualTo(shortName);
@@ -48,120 +65,189 @@ public class DocumentNameBuilderTest {
             assertThat(documentName.isCaseSensitive()).as("Invalid case sensitivity").isFalse();
         }
         assertThat(documentName.localized()).as("Invalid localized ").isEqualTo(localized);
-        final String sep = documentName.getDirectorySeparator().equals("/") ? "\\" : "/";
-        assertThat(documentName.localized(sep)).as(() -> String.format("Invalid localized('%s')", sep)).isEqualTo(localizedArg);
+        assertThat(documentName.localized("+")).as("Invalid localized('+')").isEqualTo(localizedArg);
     }
 
-    static Stream<Arguments> buildTestData() {
-        List<Arguments> lst = new ArrayList<>();
-
-            //
-            String testName = "windows\\foo direct";
-            DocumentName documentName = DocumentName.builder(WINDOWS).setName("C:\\windows\\foo").setBaseName("C:\\windows").build();
-            lst.add(Arguments.of( testName, documentName, "C:\\windows\\foo", "foo", "C:\\windows", "C:", "\\", false,
-                    "\\foo", "/foo"));
-            DocumentName baseName = documentName;
-
-            //
-            testName = "builder(docName)";
-            documentName = DocumentName.builder(baseName).build();
-            lst.add(Arguments.of( testName, documentName, "C:\\windows\\foo", "foo", "C:\\windows", "C:", "\\", false,
-                    "\\foo", "/foo"));
-
-            //
-            testName = "windows\\foo\\bar by resolve";
-            documentName = baseName.resolve("bar");
-            lst.add(Arguments.of(testName, documentName, "C:\\windows\\foo\\bar", "bar", "C:\\windows", "C:", "\\", false,
-                    "\\foo\\bar", "/foo/bar"));
-
-            //
-            testName = "windows\\foo\\direct by basename";
-            documentName = DocumentName.builder(baseName).setName("windows\\foo\\direct").build();
-            lst.add(Arguments.of(testName, documentName, "C:\\windows\\foo\\direct", "direct", "C:\\windows", "C:", "\\", false,
-                    "\\foo\\direct", "/foo/direct"));
-
-            //
-            testName = "windows\\foo\\bar by file";
-            File file = mock(File.class);
-            File parent = mock(File.class);
-            when(file.getAbsolutePath()).thenReturn("C:\\windows\\foo\\bar");
-            when(file.getParentFile()).thenReturn(parent);
-            when(file.isDirectory()).thenReturn(false);
-            when(parent.getAbsolutePath()).thenReturn("C:\\windows\\foo");
-            when(parent.isDirectory()).thenReturn(true);
-            documentName = new DocumentName.Builder(WINDOWS, file).build();
-            lst.add(Arguments.of(testName, documentName, "C:\\windows\\foo\\bar", "bar", "C:\\windows\\foo", "C:", "\\", false,
-                    "\\bar", "/bar"));
-
-        //
-        testName = "windows\\foo\\bar by directory";
-        file = mock(File.class);
-        parent = mock(File.class);
-        when(file.getAbsolutePath()).thenReturn("C:\\windows\\foo\\bar");
-        when(file.getParentFile()).thenReturn(parent);
-        when(file.isDirectory()).thenReturn(true);
-        when(parent.getAbsolutePath()).thenReturn("C:\\windows\\foo");
-        when(parent.isDirectory()).thenReturn(true);
-        documentName = new DocumentName.Builder(WINDOWS, file).build();
-        lst.add(Arguments.of(testName, documentName, "C:\\windows\\foo\\bar", "bar", "C:\\windows\\foo\\bar", "C:", "\\", false,
-                "\\", "/"));
-
-            //
-            testName = "windows setRoot";
-            documentName = DocumentName.builder(baseName).setRoot("D:").build();
-            lst.add(Arguments.of(testName, documentName, "D:\\windows\\foo", "foo", "C:\\windows", "D:", "\\", false,
-                    "D:\\windows\\foo", "D:/windows/foo"));
-
-            testName = "windows setRoot(null)";
-            documentName = DocumentName.builder(baseName).setRoot(null).build();
-            lst.add(Arguments.of(testName, documentName, "\\windows\\foo", "foo", "C:\\windows", "", "\\", false,
-                    "\\windows\\foo", "/windows/foo"));
-
-            testName = "windows setRoot('')";
-            documentName = DocumentName.builder(baseName).setRoot("").build();
-            lst.add(Arguments.of(testName, documentName, "\\windows\\foo", "foo", "C:\\windows", "", "\\", false,
-                    "\\windows\\foo", "/windows/foo"));
-
-            //
-            testName = "windows setName('baz')";
-            documentName = DocumentName.builder(baseName).setName("baz").build();
-            lst.add(Arguments.of(testName, documentName, "C:\\windows\\baz", "baz", "C:\\windows", "C:", "\\", false,
-                    "\\baz", "/baz"));
-
-            testName = "windows setName((String)null)";
-            documentName = DocumentName.builder(baseName).setName((String)null).build();
-            lst.add(Arguments.of(testName, documentName, "C:\\windows", "windows", "C:\\windows", "C:", "\\", false,
-                    "\\", "/"));
-
-            testName = "windows setName('')";
-            documentName = DocumentName.builder(baseName).setName("").build();
-            lst.add(Arguments.of(testName, documentName, "C:\\windows", "windows", "C:\\windows", "C:", "\\", false,
-                    "\\", "/"));
-
-        file = mock(File.class);
-        parent = mock(File.class);
-        when(file.getAbsolutePath()).thenReturn("C:\\windows\\foo\\bar");
+    /**
+     * Verifies tha the baseName is not modified when used in the builder.
+     * Base name is default root + OS name.  For example {@code C:\windows}, or {@code /unix}
+     * @param fsInfo the file system info for the test.
+     */
+    @ParameterizedTest
+    @FieldSource("TEST_SUITE")
+    void baseNamePreserved(DocumentName.FSInfo fsInfo) {
+        final String root = fsInfo.roots()[0];
+        final String baseNameStr = root + fsInfo;
+        // create a document {os name}/bar. Used to establish basename in builder.
+        final DocumentName siblingName = DocumentName.builder(fsInfo).setName("bar").setBaseName(fsInfo.toString()).build();
+
+        // check a relative name does not change base name.
+        String nameStr = fsInfo.mkPath("foo", "baz");
+        DocumentName documentName = DocumentName.builder(siblingName).setName(nameStr).build();
+        String expected = root + fsInfo.mkPath(fsInfo.toString(), "foo", "baz");
+        assertThat(documentName.getName()).as("relative value").isEqualTo(expected);
+        assertDocumentName(documentName, expected, "baz", baseNameStr, root, fsInfo.dirSeparator(), fsInfo.isCaseSensitive(),
+                fsInfo.dirSeparator() + nameStr, "+foo+baz");
+
+        // check a fully qualified name results in the base name not being changed.
+        documentName = DocumentName.builder(siblingName).setName(expected).build();
+        assertThat(documentName.getName()).as("absolute value").isEqualTo(expected);
+        assertDocumentName(documentName, expected, "baz", baseNameStr, root, fsInfo.dirSeparator(), fsInfo.isCaseSensitive(),
+                fsInfo.dirSeparator() + nameStr, "+foo+baz");
+    }
+
+    @ParameterizedTest
+    @FieldSource("TEST_SUITE")
+    void documentNameFromFullyQualifiedNameWithBaseName(DocumentName.FSInfo fsInfo) {
+        final String root = fsInfo.roots()[0];
+        final String baseNameStr = root + fsInfo;
+        String fqName = root + fsInfo.mkPath(fsInfo.toString(), "foo");
+        DocumentName documentName = DocumentName.builder(fsInfo).setName(fqName).setBaseName(baseNameStr).build();
+        assertDocumentName(documentName, fqName, "foo", baseNameStr, root, fsInfo.dirSeparator(), fsInfo.isCaseSensitive(),
+                fsInfo.dirSeparator() + "foo", "+foo");
+    }
+
+    @ParameterizedTest
+    @FieldSource("TEST_SUITE")
+    void noBaseNameThrowsException(DocumentName.FSInfo fsInfo) {
+        final String root = fsInfo.roots()[0];
+        String fqName = root + fsInfo.mkPath(fsInfo.toString(), "foo");
+        DocumentName.Builder underTest = DocumentName.builder(fsInfo).setName(fqName);
+        assertThatThrownBy(underTest::build)
+                .isInstanceOf(NullPointerException.class)
+                .hasMessage("Basename must not be null");
+    }
+
+    @ParameterizedTest
+    @FieldSource("TEST_SUITE")
+    void noNameThrowsException(DocumentName.FSInfo fsInfo) {
+        final String root = fsInfo.roots()[0];
+        String fqName = root + fsInfo.mkPath(fsInfo.toString(), "foo");
+        DocumentName.Builder underTest = DocumentName.builder(fsInfo).setBaseName(fqName);
+        assertThatThrownBy(underTest::build)
+                .isInstanceOf(NullPointerException.class)
+                .hasMessage("Name must not be null");
+    }
+
+    @Test
+    void DocumentNameFromFileSystem() {
+        FileSystem fileSystem = FileSystems.getDefault();
+        String expectedRoot = fileSystem.getRootDirectories().iterator().next().toString();
+        DocumentName documentName = DocumentName.builder(fileSystem).setBaseName("")
+                .setName("theName.txt").build();
+        assertThat(documentName.getRoot()).isEqualTo(expectedRoot);
+        assertThat(documentName.getName()).isEqualTo(expectedRoot + "theName.txt");
+    }
+
+    @ParameterizedTest
+    @FieldSource("TEST_SUITE")
+    void DocumentNameFromDocumentName(DocumentName.FSInfo fsInfo) {
+        final String root = fsInfo.roots()[0];
+        final String baseNameStr = root + fsInfo;
+        String fqName = root + fsInfo.mkPath(fsInfo.toString(), "foo");
+        DocumentName expected = DocumentName.builder(fsInfo).setName(fqName).setBaseName(baseNameStr).build();
+
+        DocumentName actual = DocumentName.builder(expected).build();
+        assertThat(actual).isEqualTo(expected);
+
+        assertDocumentName(actual, fqName, "foo", baseNameStr, root, fsInfo.dirSeparator(), fsInfo.isCaseSensitive(),
+                fsInfo.dirSeparator() + "foo", "+foo");
+    }
+
+    @ParameterizedTest
+    @FieldSource("TEST_SUITE")
+    void builderOnDocumentNameWithNameSharesBaseName(DocumentName.FSInfo fsInfo) {
+        final String root = fsInfo.roots()[0];
+        final String baseNameStr = root + fsInfo;
+
+        String fqName = root + fsInfo.mkPath(fsInfo.toString(), "foo");
+        DocumentName firstName = DocumentName.builder(fsInfo).setName(fqName).setBaseName(baseNameStr).build();
+
+        fqName = root + fsInfo.mkPath(fsInfo.toString(), "bar");
+        DocumentName actual = DocumentName.builder(firstName).setName(fqName).setBaseName(baseNameStr).build();
+
+        assertDocumentName(actual, fqName, "bar", baseNameStr, root, fsInfo.dirSeparator(), fsInfo.isCaseSensitive(),
+                fsInfo.dirSeparator() + "bar", "+bar");
+    }
+
+    @ParameterizedTest
+    @FieldSource("TEST_SUITE")
+    void builderOnFile(DocumentName.FSInfo fsInfo) {
+        String root = fsInfo.roots()[0];
+        final String baseNameStr = root + fsInfo.mkPath(fsInfo.toString(), "foo");
+        final String fqName = root + fsInfo.mkPath(fsInfo.toString(), "foo", "bar");
+        File file = mock(File.class);
+        File parent = mock(File.class);
+        when(file.getAbsolutePath()).thenReturn(fqName);
         when(file.getParentFile()).thenReturn(parent);
         when(file.isDirectory()).thenReturn(false);
-        when(parent.getAbsolutePath()).thenReturn("C:\\windows\\foo");
-        when(parent.isDirectory()).thenReturn(true);
-            testName = "windows setName(file)";
-            documentName = DocumentName.builder(baseName).setName(file).build();
-            lst.add(Arguments.of(testName, documentName, "C:\\windows\\foo\\bar", "bar", "C:\\windows\\foo", "C:", "\\", false,
-                    "\\bar", "/bar"));
-
-        file = mock(File.class);
-        parent = mock(File.class);
-        when(file.getAbsolutePath()).thenReturn("C:\\windows\\foo\\bar");
-        when(file.getParentFile()).thenReturn(parent);
-        when(file.isDirectory()).thenReturn(true);
-        when(parent.getAbsolutePath()).thenReturn("C:\\windows\\foo");
+        when(parent.getAbsolutePath()).thenReturn(baseNameStr);
         when(parent.isDirectory()).thenReturn(true);
-        testName = "windows setName(directory)";
-        documentName = DocumentName.builder(baseName).setName(file).build();
-        lst.add(Arguments.of(testName, documentName, "C:\\windows\\foo\\bar", "bar", "C:\\windows\\foo\\bar", "C:", "\\", false,
-                "\\", "/"));
 
-        return lst.stream();
+        DocumentName actual = new DocumentName.Builder(fsInfo, file).build();
+
+        assertDocumentName(actual, fqName, "bar", baseNameStr, root, fsInfo.dirSeparator(), fsInfo.isCaseSensitive(),
+                fsInfo.dirSeparator() + "bar", "+bar");
+    }
+
+    @Test
+    void windowRootDifference() {
+        // verify that setting the root results in the entire DocumentName being re-rooted e.g. including the basename(s).
+        DocumentName.FSInfo fsInfo = WINDOWS;
+        String root = fsInfo.roots()[0];
+        String baseNameStr = root + fsInfo.mkPath(fsInfo.toString(), "foo");
+        String fqName = root + fsInfo.mkPath(fsInfo.toString(), "foo", "bar");
+        DocumentName firstName = DocumentName.builder(fsInfo).setName(fqName).setBaseName(baseNameStr).build();
+
+        root = fsInfo.roots()[1];
+        DocumentName actual = DocumentName.builder(firstName).setRoot(root).build();
+
+        baseNameStr = root + fsInfo.mkPath(fsInfo.toString(), "foo");
+        fqName = root + fsInfo.mkPath(fsInfo.toString(), "foo", "bar");
+
+        assertDocumentName(actual, fqName, "bar", baseNameStr, root, fsInfo.dirSeparator(), fsInfo.isCaseSensitive(),
+                fsInfo.dirSeparator() + "bar", "+bar");
+    }
+
+    @ParameterizedTest
+    @FieldSource("TEST_SUITE")
+    void setRootNull(DocumentName.FSInfo fsInfo) {
+        for (String root : fsInfo.roots()) {
+            String baseNameStr = root + fsInfo.mkPath(fsInfo.toString(), "foo");
+            String fqName = root + fsInfo.mkPath(fsInfo.toString(), "foo", "bar");
+            DocumentName firstName = DocumentName.builder(fsInfo).setName(fqName).setBaseName(baseNameStr).build();
+
+            // verify setting the root to null results in relative names with a blank root set
+            DocumentName actual = DocumentName.builder(firstName).setRoot(null).build();
+
+            assertDocumentName(actual, fqName, "bar", baseNameStr, root, fsInfo.dirSeparator(), fsInfo.isCaseSensitive(),
+                    fsInfo.dirSeparator() + "bar", "+bar");
+        }
+    }
+
+    @ParameterizedTest
+    @FieldSource("TEST_SUITE")
+    void setRootEmpty(DocumentName.FSInfo fsInfo) {
+        for (String baseRoot : fsInfo.roots()) {
+            String baseNameStr = baseRoot + fsInfo.mkPath(fsInfo.toString(), "foo");
+            String fqName = baseRoot + fsInfo.mkPath(fsInfo.toString(), "foo", "bar");
+            DocumentName firstName = DocumentName.builder(fsInfo).setName(fqName).setBaseName(baseNameStr).build();
+
+            DocumentName actual = DocumentName.builder(firstName).setRoot("").build();
+
+            assertDocumentName(actual, fqName, "bar", baseNameStr, baseRoot, fsInfo.dirSeparator(), fsInfo.isCaseSensitive(),
+                    fsInfo.dirSeparator() + "bar", "+bar");
+        }
+    }
+
+    @ParameterizedTest
+    @FieldSource("TEST_SUITE")
+    void splitRootsTest(DocumentName.FSInfo fsInfo) {
+        for (String root : fsInfo.roots()) {
+            String path = fsInfo.mkPath("My", "path", "to", "a", "file.txt");
+            Pair<String, String> result = DocumentName.builder(fsInfo).splitRoot(root + path);
+            assertThat(result.getLeft()).isEqualTo(root);
+            assertThat(result.getRight()).isEqualTo(path);
+        }
     }
 }
diff --git a/apache-rat-core/src/test/java/org/apache/rat/document/DocumentNameMatcherTest.java b/apache-rat-core/src/test/java/org/apache/rat/document/DocumentNameMatcherTest.java
index c53fb23d..c5c616bb 100644
--- a/apache-rat-core/src/test/java/org/apache/rat/document/DocumentNameMatcherTest.java
+++ b/apache-rat-core/src/test/java/org/apache/rat/document/DocumentNameMatcherTest.java
@@ -18,21 +18,22 @@
  */
 package org.apache.rat.document;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.function.Predicate;
 import org.apache.commons.io.filefilter.NameFileFilter;
 import org.apache.rat.config.exclusion.plexus.MatchPatterns;
-import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.apache.rat.document.DocumentNameMatcher.MATCHES_ALL;
 import static org.apache.rat.document.DocumentNameMatcher.MATCHES_NONE;
 
 public class DocumentNameMatcherTest {
-
     private final static DocumentNameMatcher TRUE = new DocumentNameMatcher("T", (Predicate<DocumentName>)name -> true);
     private final static DocumentNameMatcher FALSE = new DocumentNameMatcher("F", (Predicate<DocumentName>)name -> false);
     private final static DocumentNameMatcher SOME = new DocumentNameMatcher("X", (Predicate<DocumentName>)name -> false);
-    private final static DocumentName testName = DocumentName.builder().setName("testName").setBaseName("/").build();
 
     public static String processDecompose(DocumentNameMatcher matcher, DocumentName candidate) {
         StringBuilder sb = new StringBuilder();
@@ -40,24 +41,35 @@ public class DocumentNameMatcherTest {
         return sb.toString();
     }
 
-    @Test
-    public void orTest() {
+    private static List<DocumentName> testDocuments() {
+        List<DocumentName> args = new ArrayList<>();
+        for (DocumentName.FSInfo fsInfo : FSInfoTest.TEST_SUITE) {
+            args.add(DocumentName.builder(fsInfo).setName("testName").setBaseName("/").build());
+        }
+        return args;
+    }
+
+    @ParameterizedTest
+    @MethodSource("testDocuments")
+    void orTest(DocumentName testName) {
         assertThat(DocumentNameMatcher.or(TRUE, FALSE).matches(testName)).as("T,F").isTrue();
         assertThat(DocumentNameMatcher.or(FALSE, TRUE).matches(testName)).as("F,T").isTrue();
         assertThat(DocumentNameMatcher.or(TRUE, TRUE).matches(testName)).as("T,T").isTrue();
         assertThat(DocumentNameMatcher.or(FALSE, FALSE).matches(testName)).as("F,F").isFalse();
     }
 
-    @Test
-    public void andTest() {
+    @ParameterizedTest
+    @MethodSource("testDocuments")
+    void andTest(DocumentName testName) {
         assertThat(DocumentNameMatcher.and(TRUE, FALSE).matches(testName)).as("T,F").isFalse();
         assertThat(DocumentNameMatcher.and(FALSE, TRUE).matches(testName)).as("F,T").isFalse();
         assertThat(DocumentNameMatcher.and(TRUE, TRUE).matches(testName)).as("T,T").isTrue();
         assertThat(DocumentNameMatcher.and(FALSE, FALSE).matches(testName)).as("F,F").isFalse();
     }
 
-    @Test
-    public void matcherSetTest() {
+    @ParameterizedTest
+    @MethodSource("testDocuments")
+    void matcherSetTest(DocumentName testName) {
         assertThat(DocumentNameMatcher.matcherSet(TRUE, FALSE).matches(testName)).as("T,F").isTrue();
         assertThat(DocumentNameMatcher.matcherSet(FALSE, TRUE).matches(testName)).as("F,T").isFalse();
         assertThat(DocumentNameMatcher.matcherSet(TRUE, TRUE).matches(testName)).as("T,T").isTrue();
@@ -76,8 +88,9 @@ public class DocumentNameMatcherTest {
         assertThat(DocumentNameMatcher.matcherSet(SOME, SOME).toString()).as("X,X").isEqualTo("matcherSet(X, X)");
     }
 
-    @Test
-    void testDecompose() {
+    @ParameterizedTest
+    @MethodSource("testDocuments")
+    void testDecompose(DocumentName testName) {
         DocumentNameMatcher matcher1 = new DocumentNameMatcher("FileFilterTest", new NameFileFilter("File.name"));
         String result = processDecompose(matcher1, testName);
         assertThat(result).contains("FileFilterTest: >>false<<").contains("  NameFileFilter(File.name)");
diff --git a/apache-rat-core/src/test/java/org/apache/rat/document/DocumentNameTest.java b/apache-rat-core/src/test/java/org/apache/rat/document/DocumentNameTest.java
index beeeeca9..5abac962 100644
--- a/apache-rat-core/src/test/java/org/apache/rat/document/DocumentNameTest.java
+++ b/apache-rat-core/src/test/java/org/apache/rat/document/DocumentNameTest.java
@@ -18,79 +18,79 @@
  */
 package org.apache.rat.document;
 
+import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileFilter;
+import java.io.FileReader;
+import java.io.FileWriter;
 import java.io.FilenameFilter;
 import java.io.IOException;
-import java.nio.file.FileSystems;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 import java.util.stream.Stream;
 
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.lang3.tuple.Pair;
 import org.apache.rat.config.exclusion.ExclusionUtils;
 import org.apache.rat.document.DocumentName.FSInfo;
 
-import org.assertj.core.util.Files;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.FieldSource;
 import org.junit.jupiter.params.provider.MethodSource;
-import org.mockito.Mockito;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.apache.rat.document.FSInfoTest.OSX;
-import static org.apache.rat.document.FSInfoTest.UNIX;
-import static org.apache.rat.document.FSInfoTest.WINDOWS;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.mockito.ArgumentMatchers.any;
 
 public class DocumentNameTest {
-
-    public static DocumentName mkName(Path tempDir, FSInfo fsInfo) {
-        File docFile = mkFile(tempDir.toFile(), fsInfo);
-        DocumentName result = DocumentName.builder(fsInfo).setName(docFile).build();
-        DocumentName mocked = Mockito.spy(result);
-
-        String fn = result.localized(FileSystems.getDefault().getSeparator());
-        File file = tempDir.resolve(fn.substring(1)).toFile();
-        File mockedFile = mkFile(file, fsInfo);
-        when(mocked.asFile()).thenReturn(mockedFile);
-
-        assertThat(mocked.asFile()).isEqualTo(mockedFile);
-        return mocked;
-    }
-
-    private static File[] listFiles(File file, FSInfo fsInfo) {
-        File[] fileList = file.listFiles();
-        if (fileList == null) {
-            return fileList;
-        }
-        return Arrays.stream(fileList).map(f -> mkFile(f, fsInfo)).toArray(File[]::new);
+    private static final FSInfo[] TEST_SUITE = FSInfoTest.TEST_SUITE;
+
+    /**
+     * Create a list of mocked files from the specified directory.
+     * @param directory the native directory to read.
+     * @param fsInfo the file system to mock the files in.
+     * @return an array of mocked files in the file system.
+     */
+    private static File[] listFiles(File directory, FSInfo fsInfo) {
+        File[] fileList = directory.listFiles();
+        return fileList == null ? null : Arrays.stream(fileList).map(f -> mkFile(f, fsInfo)).toArray(File[]::new);
     }
 
-    private static File[] listFiles(File file, FSInfo fsInfo, FileFilter filter) {
-        File[] fileList = file.listFiles();
-        if (fileList == null) {
-            return fileList;
-        }
-        return Arrays.stream(fileList).map(f -> mkFile(f, fsInfo)).filter(filter::accept).toArray(File[]::new);
+    /**
+     * Create an array of mocked files from the specified directory then apply applying a file filter.
+     * @param directory the native directory to read files from.
+     * @param fsInfo the file system to create the mocked files in.
+     * @param filter the filter to apply to the mocked files.
+     * @return the array of mocked files that pass the filter.
+     */
+    private static File[] listFiles(File directory, FSInfo fsInfo, FileFilter filter) {
+        File[] fileList = directory.listFiles();
+        return fileList == null ? null : Arrays.stream(fileList).map(f -> mkFile(f, fsInfo)).filter(filter::accept).toArray(File[]::new);
     }
 
-    private static File[] listFiles(File file, FSInfo fsInfo, FilenameFilter filter) {
-        File[] fileList = file.listFiles();
-        if (fileList == null) {
-            return fileList;
-        }
-        return Arrays.stream(fileList).map(f -> mkFile(f, fsInfo)).filter(x -> filter.accept(x, x.getName())).toArray(File[]::new);
+    /**
+     * Create an array of mocked files from the specified directory then apply applying a file filter.
+     * @param directory the native directory to read files from.
+     * @param fsInfo the file system to create the mocked files in.
+     * @param filter the filter to apply to the mocked files.
+     * @return the array of mocked files that pass the filter.
+     */
+    private static File[] listFiles(File directory, FSInfo fsInfo, FilenameFilter filter) {
+        File[] fileList = directory.listFiles();
+        return fileList == null ? null : Arrays.stream(fileList).map(f -> mkFile(f, fsInfo)).filter(x -> filter.accept(x, x.getName())).toArray(File[]::new);
     }
 
+    /**
+     * Creates a mocked file on the specified file system with the
+     * @param file the name of the native file.
+     * @param fsInfo the file system to mock the file in.
+     * @return the mocked file in the specified file system
+     */
     public static File mkFile(final File file, final FSInfo fsInfo) {
         File mockedFile = mock(File.class);
         when(mockedFile.listFiles()).thenAnswer( env -> listFiles(file, fsInfo));
@@ -105,222 +105,199 @@ public class DocumentNameTest {
         return mockedFile;
     }
 
-    public static DocumentName mkName(Path tempDir, DocumentName baseDir, String pth) throws IOException {
-        DocumentName result = baseDir.resolve(ExclusionUtils.convertSeparator(pth, "/", baseDir.getDirectorySeparator()));
-        DocumentName mocked = Mockito.spy(result);
-
-        String fn = result.localized(FileSystems.getDefault().getSeparator());
-        File file = tempDir.resolve(fn.substring(1)).toFile();
-        File parent = file.getParentFile();
-        if (parent.exists() && !parent.isDirectory()) {
-            parent.delete();
-        }
-        parent.mkdirs();
-        if (file.exists()) {
-            if (file.isDirectory()) {
-                FileUtils.deleteDirectory(file);
-            } else {
-                FileUtils.delete(file);
-            }
-        }
-        file.createNewFile();
-        when(mocked.asFile()).thenReturn(file);
-        return mocked;
-    }
-
-    @ParameterizedTest(name = "{index} {0} {2}")
+    /**
+     * Verifies that {@code resolve()} works correctly.
+     * @param fsInfo the file system under test.
+     * @param base the DocumentName to resolve from.
+     * @param toResolve the string to resolve.
+     * @param expected the expected DocumentName after resolution.
+     */
+    @ParameterizedTest(name = "{index} {0} {1} {3}")
     @MethodSource("resolveTestData")
-    void resolveTest(String testName, DocumentName base, String toResolve, DocumentName expected) {
+    void resolveTest(DocumentName.FSInfo fsInfo, String root, DocumentName base, String toResolve, DocumentName expected) {
        DocumentName actual = base.resolve(toResolve);
-       assertThat(actual).isEqualTo(expected);
+       assertThat(actual.getName()).isEqualTo(expected.getName());
     }
 
     private static Stream<Arguments> resolveTestData() {
         List<Arguments> lst = new ArrayList<>();
+        DocumentName base;
+        DocumentName expected;
+        for (DocumentName.FSInfo fsInfo : TEST_SUITE) {
+            String root = fsInfo.roots()[0];
+            for (String baseName : List.of(root, root + fsInfo.mkPath("from", "base"))) {
+                for (String testRoot : fsInfo.roots()) {
+                    String name = testRoot + fsInfo.mkPath("", "dir", fsInfo.toString());
+                    base = DocumentName.builder(fsInfo).setName(name).setBaseName(baseName).build();
+                    lst.add(Arguments.of(fsInfo, base.getName(),  base, null, base));
 
-        DocumentName base = DocumentName.builder(UNIX).setName("/dir/unix").setBaseName("/").build();
-
-        DocumentName expected = DocumentName.builder(UNIX).setName("/dir/unix/relative").setBaseName("/").build();
-        lst.add(Arguments.of("unix", base, "relative", expected));
+                    lst.add(Arguments.of(fsInfo, base.getName(), base, "", base));
 
-        expected = DocumentName.builder(UNIX).setName("/from/root").setBaseName("/").build();
-        lst.add(Arguments.of("unix", base, "/from/root", expected));
+                    lst.add(Arguments.of(fsInfo, base.getName(), base, "  ", base));
 
-        expected = DocumentName.builder(UNIX).setName("dir/up/and/down").setBaseName("/").build();
-        lst.add(Arguments.of("unix", base, "../up/and/down", expected));
+                    expected = DocumentName.builder(fsInfo).setName(fsInfo.mkPath(name, "relative")).setBaseName(baseName).build();
+                    lst.add(Arguments.of(fsInfo, base.getName(), base, "relative", expected));
 
-        expected = DocumentName.builder(UNIX).setName("/from/root").setBaseName("/").build();
-        lst.add(Arguments.of("unix", base, "\\from\\root", expected));
+                    expected = DocumentName.builder(fsInfo).setRoot(testRoot).setName(fsInfo.mkPath("", "from", "root")).setBaseName(baseName).build();
+                    lst.add(Arguments.of(fsInfo, base.getName(), base, fsInfo.mkPath("", "from", "root"), expected));
 
-        expected = DocumentName.builder(UNIX).setName("dir/up/and/down").setBaseName("/").build();
-        lst.add(Arguments.of("unix", base, "..\\up\\and\\down", expected));
+                    expected = DocumentName.builder(fsInfo).setRoot(testRoot).setName(fsInfo.mkPath("dir", "up", "and", "down")).setBaseName(baseName).build();
+                    lst.add(Arguments.of(fsInfo, base.getName(), base, fsInfo.mkPath("..", "up", "and", "down"), expected));
 
-        // WINDOWS
-        base = DocumentName.builder(WINDOWS).setName("\\dir\\windows").setBaseName("C:\\").build();
-
-        expected = DocumentName.builder(WINDOWS).setName("\\dir\\windows\\relative").setBaseName("C:\\").build();
-        lst.add(Arguments.of("windows", base, "relative", expected));
-
-        expected = DocumentName.builder(WINDOWS).setName("\\from\\root").setBaseName("C:\\").build();
-        lst.add(Arguments.of("windows", base, "/from/root", expected));
-
-        expected = DocumentName.builder(WINDOWS).setName("dir\\up\\and\\down").setBaseName("C:\\").build();
-        lst.add(Arguments.of("windows", base, "../up/and/down", expected));
-
-        expected = DocumentName.builder(WINDOWS).setName("\\from\\root").setBaseName("C:\\").build();
-        lst.add(Arguments.of("windows", base, "\\from\\root", expected));
-
-        expected = DocumentName.builder(WINDOWS).setName("dir\\up\\and\\down").setBaseName("C:\\").build();
-        lst.add(Arguments.of("windows", base, "..\\up\\and\\down", expected));
-
-        // OSX
-        base = DocumentName.builder(OSX).setName("/dir/osx").setBaseName("/").build();
-
-        expected = DocumentName.builder(OSX).setName("/dir/osx/relative").setBaseName("/").build();
-        lst.add(Arguments.of("osx", base, "relative", expected));
-
-        expected = DocumentName.builder(OSX).setName("/from/root").setBaseName("/").build();
-        lst.add(Arguments.of("osx", base, "/from/root", expected));
-
-        expected = DocumentName.builder(OSX).setName("dir/up/and/down").setBaseName("/").build();
-        lst.add(Arguments.of("osx", base, "../up/and/down", expected));
-
-        expected = DocumentName.builder(OSX).setName("/from/root").setBaseName("/").build();
-        lst.add(Arguments.of("osx", base, "\\from\\root", expected));
-
-        expected = DocumentName.builder(OSX).setName("dir/up/and/down").setBaseName("/").build();
-        lst.add(Arguments.of("osx", base, "..\\up\\and\\down", expected));
+                    expected = DocumentName.builder(fsInfo).setRoot(testRoot).setName(fsInfo.mkPath("", "from", "root")).setBaseName(baseName).build();
+                    String wrongSeparator = fsInfo.dirSeparator().equals("/") ? "\\" : "/";
+                    lst.add(Arguments.of(fsInfo, base.getName(), base, String.join(wrongSeparator, "", "from", "root"), expected));
 
+                    expected = DocumentName.builder(fsInfo).setRoot(testRoot).setName(fsInfo.mkPath("dir", "up", "and", "down")).setBaseName(baseName).build();
+                    lst.add(Arguments.of(fsInfo, base.getName(), base, String.join(wrongSeparator, "..", "up", "and", "down"), expected));
+                }
+            }
+        }
         return lst.stream();
     }
 
     @Test
-    void localizeTest() {
-        DocumentName documentName = DocumentName.builder(UNIX).setName("/a/b/c")
-                .setBaseName("/a").build();
-        assertThat(documentName.localized()).isEqualTo("/b/c");
-        assertThat(documentName.localized("-")).isEqualTo("-b-c");
+    void resolveWithMultipleRootsTest() {
+        // these tests exist because windows has multiple roots.
+        DocumentName.FSInfo fsInfo = FSInfoTest.WINDOWS;
+        DocumentName base = DocumentName.builder(fsInfo).setName(fsInfo.mkPath("", "dir", fsInfo.toString()))
+                .setBaseName("").build();
+
+        String resolveName = fsInfo.roots()[1] + fsInfo.mkPath("dir", fsInfo.toString());
+        assertThatThrownBy(() -> base.resolve(resolveName))
+                .as(resolveName)
+                .hasMessageContaining(String.format("%s does not start with %s", resolveName, base.getName()))
+                .isInstanceOf(IllegalArgumentException.class);
+
+        String resolveName2 = fsInfo.roots()[0] + fsInfo.mkPath("dir", fsInfo.toString(), "thing");
+        assertThat(base.resolve(resolveName2).getName())
+                .isEqualTo(resolveName2);
 
-        documentName = DocumentName.builder(WINDOWS).setName("\\a\\b\\c")
-                .setBaseName("\\a").build();
-        assertThat(documentName.localized()).isEqualTo("\\b\\c");
-        assertThat(documentName.localized("-")).isEqualTo("-b-c");
+    }
 
-        documentName = DocumentName.builder(OSX).setName("/a/b/c")
-                .setBaseName("/a").build();
-        assertThat(documentName.localized()).isEqualTo("/b/c");
-        assertThat(documentName.localized("-")).isEqualTo("-b-c");
+    void testNoRootSpecified() {
+        // no root specified
+        assertThat(DocumentName.startsWithRootOrSeparator("X:/candidate", null, "/")).isFalse();
+        assertThat(DocumentName.startsWithRootOrSeparator("X:/candidate", "", "/")).isFalse();
+        assertThat(DocumentName.startsWithRootOrSeparator("X;/candidate", "  ", "/")).isFalse();
+        assertThat(DocumentName.startsWithRootOrSeparator("/candidate", null, "/")).isTrue();
+        assertThat(DocumentName.startsWithRootOrSeparator("/candidate", "", "/")).isTrue();
+        assertThat(DocumentName.startsWithRootOrSeparator("/candidate", "  ", "/")).isTrue();
+        assertThat(DocumentName.startsWithRootOrSeparator("candidate", null, "/")).isFalse();
+        assertThat(DocumentName.startsWithRootOrSeparator("candidate", "", "/")).isFalse();
+        assertThat(DocumentName.startsWithRootOrSeparator("candidate", "  ", "/")).isFalse();
     }
 
-    @ParameterizedTest(name = "{index} {0}")
-    @MethodSource("validBuilderData")
-    void validBuilderTest(String testName, DocumentName.Builder builder, String root, String name, String baseName, String dirSeparator) {
-        DocumentName underTest = builder.build();
-        assertThat(underTest.getRoot()).as(testName).isEqualTo(root);
-        assertThat(underTest.getDirectorySeparator()).as(testName).isEqualTo(dirSeparator);
-        assertThat(underTest.getName()).as(testName).isEqualTo(root + dirSeparator + name);
-        assertThat(underTest.getBaseName()).as(testName).isEqualTo(root + dirSeparator + baseName);
+    @Test
+    void startsWithRootOrSeparatorTest() {
+        assertThat(DocumentName.startsWithRootOrSeparator("X:/candidate", "X:/", "/")).isTrue();
+        assertThat(DocumentName.startsWithRootOrSeparator("X:/candidate", "Y:/", "/")).isFalse();
+        assertThat(DocumentName.startsWithRootOrSeparator("/candidate", "X:/", "/")).isTrue();
+        assertThat(DocumentName.startsWithRootOrSeparator("\\candidate", "Y:/", "/")).isFalse();
+        assertThat(DocumentName.startsWithRootOrSeparator("candidate", "Y:/", "/")).isFalse();
+        assertThat(DocumentName.startsWithRootOrSeparator("Y:candidate", "Y:/", "/")).isFalse();
+        assertThat(DocumentName.startsWithRootOrSeparator(null, "Y:/", "/")).isFalse();
+        assertThat(DocumentName.startsWithRootOrSeparator("", "Y:/", "/")).isFalse();
+        assertThat(DocumentName.startsWithRootOrSeparator("  ", "Y:/", "/")).isFalse();
+
+        testNoRootSpecified();
     }
 
-    private static Stream<Arguments> validBuilderData() {
-        List<Arguments> lst = new ArrayList<>();
-        File f = Files.newTemporaryFile();
-
-        Set<String> roots = new HashSet<>();
-        File[] rootary = File.listRoots();
-        if (rootary != null) {
-            for (File root : rootary) {
-                String name = root.getPath();
-                roots.add(name);
-            }
-        }
+    @ParameterizedTest
+    @FieldSource("TEST_SUITE")
+    void localizeTest(FSInfo fsInfo) {
+        DocumentName documentName = DocumentName.builder(fsInfo).setName(
+                fsInfo.mkPath("", "a", "b", "c"))
+                .setBaseName(fsInfo.mkPath("", "a")).build();
 
-        String name = f.getAbsolutePath();
-        String root = "";
-        for (String sysRoot : roots) {
-            if (name.startsWith(sysRoot)) {
-                name = name.substring(sysRoot.length());
-                if (sysRoot.endsWith(File.separator)) {
-                    root = sysRoot.substring(0, sysRoot.length() - File.separator.length());
-                }
-                break;
-            }
-        }
-
-        File p = f.getParentFile();
-        String baseName = p.getAbsolutePath().substring(root.length());
-        if (baseName.startsWith(File.separator)) {
-            baseName = baseName.substring(File.separator.length());
-        }
-        lst.add(Arguments.of("setName(file)", DocumentName.builder().setName(f), root, name, baseName, File.separator));
-        lst.add(Arguments.of("Builder(file)", DocumentName.builder(f), root, name, baseName, File.separator));
+        assertThat(documentName.localized()).isEqualTo(fsInfo.mkPath("", "b", "c"));
+        assertThat(documentName.localized("-")).isEqualTo("-b-c");
 
-        lst.add(Arguments.of("setName(dir)", DocumentName.builder().setName(p), root, baseName, baseName, File.separator));
-        lst.add(Arguments.of("Builder(dir)", DocumentName.builder(p), root, baseName, baseName, File.separator));
+        documentName = DocumentName.builder(fsInfo).setName(
+                        fsInfo.mkPath("", "a", "b", "c"))
+                .setBaseName(fsInfo.mkPath("", "z")).build();
 
-        File r = new File(root.isEmpty() ? File.separator : root);
-        lst.add(Arguments.of("setName(root)", DocumentName.builder().setName(r), root, "", "", File.separator));
-        lst.add(Arguments.of("Builder(root)", DocumentName.builder(r), root, "", "", File.separator));
+        assertThat(documentName.localized()).isEqualTo(fsInfo.mkPath("", "a", "b", "c"));
 
+        if (fsInfo.roots().length > 1) {
+            documentName = DocumentName.builder(fsInfo).setName(
+                            fsInfo.mkPath("", "a", "b", "c"))
+                    .setBaseName(fsInfo.mkPath("", "z"))
+                    .setRoot(fsInfo.roots()[1]).build();
+            assertThat(documentName.localized()).isEqualTo(fsInfo.mkPath("", "a", "b", "c"));
+        }
+    }
 
-        lst.add(Arguments.of("foo/bar foo", DocumentName.builder(UNIX)
-                .setName("/foo/bar").setBaseName("foo"), "", "foo/bar", "foo", "/"));
+    @Test
+    void asFileTest() throws IOException {
+        File expected = File.createTempFile("docNameTest", ".txt");
+        try (FileWriter fw = new FileWriter(expected, StandardCharsets.UTF_8)) {
+            fw.write("Hello world");
+        }
+        DocumentName underTest = DocumentName.builder(expected).build();
+        File actual = underTest.asFile();
+        try (FileReader fr = new FileReader(actual, StandardCharsets.UTF_8);
+        BufferedReader br = new BufferedReader(fr)) {
+            assertThat(br.readLine()).isEqualTo("Hello world");
+        }
+    }
 
-        DocumentName.Builder builder = DocumentName.builder(WINDOWS).setName("\\foo\\bar").setBaseName("C:\\foo")
-                .setRoot("C:");
-        lst.add(Arguments.of("\\foo\\bar foo", builder, "C:", "foo\\bar", "foo", "\\"));
+    @Test
+    void asPathTest() throws IOException {
+        File expected = File.createTempFile("docNameTest", ".txt");
+        try (FileWriter fw = new FileWriter(expected, StandardCharsets.UTF_8)) {
+            fw.write("Hello world");
+        }
+        DocumentName underTest = DocumentName.builder(expected).build();
+        Path actual = underTest.asPath();
+        Path root = Path.of(underTest.getRoot());
+        File file = root.resolve(actual).toFile();
+        try (FileReader fr = new FileReader(file, StandardCharsets.UTF_8);
+             BufferedReader br = new BufferedReader(fr)) {
+            assertThat(br.readLine()).isEqualTo("Hello world");
+        }
+    }
 
-        lst.add(Arguments.of("foo/bar foo", DocumentName.builder(OSX)
-                .setName("/foo/bar").setBaseName("foo"), "", "foo/bar", "foo", "/"));
 
-        return lst.stream();
+    @ParameterizedTest(name = "{index} {0} {1}")
+    @MethodSource("archiveEntryTestData")
+    void archiveEntryNameTest(String os, String testName, DocumentName archiveName, String root, String separator, String baseName,
+                              String localizedName) {
+        assertThat(archiveName.getRoot()).as("root").isEqualTo(root);
+        assertThat(archiveName.getDirectorySeparator()).as("separator").isEqualTo(separator);
+        assertThat(archiveName.getBaseName()).as("baseName").isEqualTo(baseName);
+        assertThat(archiveName.localized()).as("localized").isEqualTo(localizedName);
+        assertThat(archiveName.getName()).as("name").isEqualTo(baseName + localizedName);
+        if (!separator.equals(archiveName.fsInfo().dirSeparator()))
+        {
+            String newBaseName = separator.equals("/") ? baseName.replace('\\', '/') : baseName.replace('/', '\\');
+            assertThat(archiveName.localized(separator)).as("localized(x)").isEqualTo(newBaseName + localizedName);
+        }
     }
 
-    @Test
-    void splitRootsTest() {
-        Pair<String, String> result = DocumentName.builder(WINDOWS).splitRoot("C:\\My\\path\\to\\a\\file.txt");
-        assertThat(result.getLeft()).isEqualTo("C:");
-        assertThat(result.getRight()).isEqualTo("My\\path\\to\\a\\file.txt");
-
-        result = DocumentName.builder(UNIX).splitRoot("/My/path/to/a/file.txt");
-        assertThat(result.getLeft()).isEqualTo("");
-        assertThat(result.getRight()).isEqualTo("My/path/to/a/file.txt");
-
-        result = DocumentName.builder(OSX).splitRoot("/My/path/to/a/file.txt");
-        assertThat(result.getLeft()).isEqualTo("");
-        assertThat(result.getRight()).isEqualTo("My/path/to/a/file.txt");
-    }
+    static List<Arguments> archiveEntryTestData() {
+        List<Arguments> lst = new ArrayList<>();
 
-    @Test
-    void archiveEntryNameTest() {
-        String entryName = "./anArchiveEntry.txt";
-        DocumentName archiveName = DocumentName.builder(WINDOWS)
-                .setName("C:\\archives\\anArchive.zip").setBaseName("C:\\archives").build();
-
-        assertThat(archiveName.getRoot()).isEqualTo("C:");
-        assertThat(archiveName.getDirectorySeparator()).isEqualTo("\\");
-        assertThat(archiveName.getBaseName()).isEqualTo("C:\\archives");
-        assertThat(archiveName.getName()).isEqualTo("C:\\archives\\anArchive.zip");
-        assertThat(archiveName.localized()).isEqualTo("\\anArchive.zip");
-
-        ArchiveEntryName archiveEntryName = new ArchiveEntryName(archiveName, entryName);
-
-        assertThat(archiveEntryName.getRoot()).isEqualTo(archiveName.getName()+"#");
-        assertThat(archiveEntryName.getDirectorySeparator()).isEqualTo("/");
-        assertThat(archiveEntryName.getBaseName()).isEqualTo("C:\\archives\\anArchive.zip#");
-        assertThat(archiveEntryName.getName()).isEqualTo("C:\\archives\\anArchive.zip#/anArchiveEntry.txt");
-        assertThat(archiveEntryName.localized()).isEqualTo("/anArchiveEntry.txt");
-        assertThat(archiveEntryName.localized("/")).isEqualTo("/anArchive.zip#/anArchiveEntry.txt");
-
-        // test with directory
-        entryName = "./someDir/anArchiveEntry.txt";
-        archiveEntryName = new ArchiveEntryName(archiveName, entryName);
-
-        assertThat(archiveEntryName.getRoot()).isEqualTo(archiveName.getName()+"#");
-        assertThat(archiveEntryName.getDirectorySeparator()).isEqualTo("/");
-        assertThat(archiveEntryName.getBaseName()).isEqualTo("C:\\archives\\anArchive.zip#");
-        assertThat(archiveEntryName.getName()).isEqualTo("C:\\archives\\anArchive.zip#/someDir/anArchiveEntry.txt");
-        assertThat(archiveEntryName.localized()).isEqualTo("/someDir/anArchiveEntry.txt");
-        assertThat(archiveEntryName.localized("/")).isEqualTo("/anArchive.zip#/someDir/anArchiveEntry.txt");
+        for (FSInfo fsInfo :  FSInfoTest.TEST_SUITE) {
+            String os = fsInfo.toString();
+            String root = fsInfo.roots()[0];
+            String baseName = String.format(String.format("%sarchives", root));
+            String simpleName = String.format("%sanArchive.zip", fsInfo.dirSeparator());
+            String entryName = "./anArchiveEntry.txt";
+            DocumentName archiveName = DocumentName.builder(fsInfo).setName(baseName + simpleName).setBaseName(baseName).build();
+            lst.add(Arguments.of(os, "archive name", archiveName, root, fsInfo.dirSeparator(), baseName, simpleName));
+
+            ArchiveEntryName archiveEntryName = new ArchiveEntryName(archiveName, entryName);
+            baseName = archiveName.getName() + "#";
+            root = baseName + "/";
+            lst.add(Arguments.of(os, "archive entry name", archiveEntryName, root, "/", baseName, "/anArchiveEntry.txt"));
+
+            // test with directory
+            entryName = "./someDir/anArchiveEntry.txt";
+            archiveEntryName = new ArchiveEntryName(archiveName, entryName);
+
+            lst.add(Arguments.of(os, "archive entry with directory", archiveEntryName, root, "/", baseName, "/someDir/anArchiveEntry.txt"));
+        }
+        return lst;
     }
 }
diff --git a/apache-rat-core/src/test/java/org/apache/rat/document/FSInfoTest.java b/apache-rat-core/src/test/java/org/apache/rat/document/FSInfoTest.java
index ced9d8c6..e2513822 100644
--- a/apache-rat-core/src/test/java/org/apache/rat/document/FSInfoTest.java
+++ b/apache-rat-core/src/test/java/org/apache/rat/document/FSInfoTest.java
@@ -23,9 +23,19 @@ import com.google.common.jimfs.Jimfs;
 import java.io.IOException;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystems;
-import java.util.Arrays;
-import java.util.stream.Stream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.FieldSource;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 public class FSInfoTest {
     public static final DocumentName.FSInfo DEFAULT;
@@ -36,7 +46,8 @@ public class FSInfoTest {
     static {
         try (FileSystem osx = Jimfs.newFileSystem(Configuration.osX());
              FileSystem unix = Jimfs.newFileSystem(Configuration.unix());
-             FileSystem windows = Jimfs.newFileSystem(Configuration.windows())) {
+             FileSystem windows = Jimfs.newFileSystem(Configuration.windows().toBuilder()
+                     .setRoots("C:\\", "D:\\").build())) {
             OSX = new DocumentName.FSInfo("osx", osx);
             UNIX = new DocumentName.FSInfo("unix", unix);
             WINDOWS = new DocumentName.FSInfo("windows", windows);
@@ -48,11 +59,96 @@ public class FSInfoTest {
 
     public static final DocumentName.FSInfo[] TEST_SUITE = {UNIX, WINDOWS, OSX};
 
-    /**
-     * Provides arguments for parameterized tests that only require the fsInfo.
-     * @return a stream of TEST_SUITE based Arguments.
-     */
-    public static Stream<Arguments> fsInfoArgs() {
-        return Arrays.stream(TEST_SUITE).map(Arguments::of);
+    @ParameterizedTest
+    @FieldSource("TEST_SUITE")
+    void normalizeTest(DocumentName.FSInfo info) {
+        String sep = info.dirSeparator();
+        Map<String, String> strings = new TreeMap<>();
+        strings.put("hello", "hello");
+        strings.put("hello/", "hello");
+        strings.put("/hello", sep+"hello");
+        strings.put("/hello/../goodbye", sep+"goodbye");
+        strings.put("hello/../goodbye", "goodbye");
+        strings.put("/hello/./goodbye", sep+"hello"+sep+"goodbye");
+        strings.put("hello/./goodbye", "hello"+sep+"goodbye");
+        strings.put("\\hello\\..\\goodbye", sep+"goodbye");
+        strings.put("hello\\..\\goodbye", "goodbye");
+        strings.put("\\hello\\.\\goodbye", sep+"hello"+sep+"goodbye");
+        strings.put("hello\\.\\goodbye", "hello"+sep+"goodbye");
+
+        strings.put(info.roots()[0]+"hello", info.roots()[0]+"hello");
+        String pattern = "a/b\\c";
+        strings.put(pattern, pattern.replace(sep.equals("/") ? "\\" : "/", sep));
+        strings.put("", "");
+        strings.put(" ", "");
+        strings.put(".", "");
+        strings.put(" .", "");
+        strings.put(" . ", "");
+        strings.put("/", sep);
+        strings.put("\\", sep);
+
+        assertThat(info.normalize(null)).as("[null]").isEmpty();
+        for (Map.Entry<String, String> entry : strings.entrySet()) {
+            assertThat(info.normalize(entry.getKey())).as(String.format("[%s]", entry.getKey()))
+                    .isEqualTo(entry.getValue());
+        }
+
+        for (String pattern2 : List.of( "/..", "hello/../..", "/hello/../..")) {
+                assertThatThrownBy(() -> info.normalize(pattern2)).as(String.format("[%s]", pattern2))
+                        .isInstanceOf(IllegalStateException.class)
+                        .hasMessageContaining("Unable to create path before root");
+                }
+    }
+
+    static List<Arguments> tokenizingData() {
+        List<Arguments> data = new ArrayList<>();
+        for (DocumentName.FSInfo fsInfo : TEST_SUITE) {
+            String sep = fsInfo.dirSeparator();
+            String notSep = sep.equals("/") ? "\\" : "/";
+            data.add(Arguments.of(fsInfo, "hello", List.of("hello")));
+            data.add(Arguments.of(fsInfo, "hello" + sep, List.of("hello")));
+            data.add(Arguments.of(fsInfo, "hello" + sep + "world", List.of("hello", "world")));
+            data.add(Arguments.of(fsInfo, sep + "hello", List.of("", "hello")));
+            data.add(Arguments.of(fsInfo, sep + "hello" + sep + ".." + sep + "goodbye", List.of("", "hello", "..", "goodbye")));
+            data.add(Arguments.of(fsInfo, "hello" + sep + ".." + sep + "goodbye", List.of("hello", "..", "goodbye")));
+            data.add(Arguments.of(fsInfo, notSep + "hello" + sep + "goodbye", List.of(notSep + "hello", "goodbye")));
+        }
+        return data;
+    }
+
+    @ParameterizedTest
+    @MethodSource("tokenizingData")
+    void tokenizeTest(DocumentName.FSInfo info, String token, List<String> split) {
+            assertThat(info.tokenize(token))
+                    .containsExactlyElementsOf(split);
+    }
+
+    @ParameterizedTest
+    @MethodSource("tokenizingData")
+    void mkPathTest(DocumentName.FSInfo info, String path, List<String> segments) {
+        if (path.endsWith(info.dirSeparator())) {
+            assertThat(info.mkPath(segments.toArray(new String[0]))).isEqualTo(path.substring(0, path.length()-1));
+        } else {
+            assertThat(info.mkPath(segments.toArray(new String[0]))).isEqualTo(path);
+        }
+    }
+
+    @Test
+    void compareTests() {
+        for (int i = 0; i < TEST_SUITE.length; i++) {
+            for (int j = 0; j < TEST_SUITE.length; j++) {
+                DocumentName.FSInfo underTest = TEST_SUITE[i];
+                DocumentName.FSInfo compareTo = TEST_SUITE[j];
+                String name = String.format("%s with %s ", underTest, compareTo);
+                if (i == j) {
+                    assertThat(underTest).as(name + "equality").isEqualTo(compareTo);
+                    assertThat(underTest.hashCode()).as(name + "hashCode").hasSameHashCodeAs(compareTo);
+                    assertThat(underTest).as(name + "compareTo").isEqualByComparingTo(compareTo);
+                } else {
+                    assertThat(underTest).as(name + "equality").isNotEqualTo(compareTo);
+                    assertThat(underTest).as(name + "compareTo").isNotEqualByComparingTo(compareTo);
+                }
+            }
+        }
     }
 }
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 6c4aa2ab..5d34d80a 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
@@ -18,6 +18,7 @@
  */
 package org.apache.rat.test;
 
+import java.io.FileWriter;
 import java.nio.file.FileSystems;
 import java.nio.file.Path;
 import org.apache.commons.cli.Option;
@@ -25,6 +26,7 @@ import org.apache.commons.io.FileUtils;
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.tuple.ImmutablePair;
 import org.apache.commons.lang3.tuple.Pair;
+import org.apache.commons.text.WordUtils;
 import org.apache.rat.OptionCollectionTest;
 import org.apache.rat.ReportConfiguration;
 import org.apache.rat.ReporterTest;
@@ -37,6 +39,7 @@ import org.apache.rat.license.ILicense;
 import org.apache.rat.license.ILicenseFamily;
 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.test.utils.Resources;
 import org.apache.rat.testhelpers.TextUtils;
 import org.apache.rat.utils.DefaultLog;
@@ -54,11 +57,13 @@ import java.nio.file.Files;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Locale;
 import java.util.SortedSet;
 
 import static org.apache.rat.commandline.Arg.HELP_LICENSES;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Fail.fail;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 
 /**
  * A list of methods that an OptionsProvider in a test case must support.
@@ -75,7 +80,7 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
      */
     public static void preserveData(File baseDir, String targetDir) {
         final Path recordPath = FileSystems.getDefault().getPath("target", targetDir);
-        recordPath.toFile().mkdirs();
+        org.apache.rat.testhelpers.FileUtils.mkDir(recordPath.toFile());
         try {
             FileUtils.copyDirectory(baseDir, recordPath.toFile());
         } catch (IOException e) {
@@ -156,7 +161,8 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
     private void execExcludeTest(final Option option, final String[] args) {
         String[] notExcluded = {"notbaz", "well._afile"};
         String[] excluded = {"some.foo", "B.bar", "justbaz"};
-        try {
+
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(ImmutablePair.of(option, args));
             DocumentNameMatcher excluder = config.getDocumentExcluder(baseName());
             for (String fname : notExcluded) {
@@ -167,9 +173,7 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
                 DocumentName docName = mkDocName(fname);
                 assertThat(excluder.matches(docName)).as(() -> dump(option, fname, excluder, docName)).isFalse();
             }
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     private void excludeFileTest(final Option option) {
@@ -198,7 +202,8 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
         String[] args = {StandardCollection.MISC.name()};
         String[] excluded = {"afile~", ".#afile", "%afile%", "._afile"};
         String[] notExcluded = {"afile~more", "what.#afile", "%afile%withMore", "well._afile"};
-        try {
+
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(ImmutablePair.of(option, args));
             DocumentNameMatcher excluder = config.getDocumentExcluder(baseName());
             for (String fname : excluded) {
@@ -209,9 +214,7 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
                 DocumentName docName = mkDocName(fname);
                 assertThat(excluder.matches(docName)).as(() -> dump(option, fname, excluder, docName)).isTrue();
             }
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     protected void inputExcludeParsedScmTest() {
@@ -229,12 +232,12 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
 
         writeFile(".gitignore", Arrays.asList(lines));
         File dir = new File(baseDir, "red");
-        dir.mkdirs();
+        org.apache.rat.testhelpers.FileUtils.mkDir(dir);
         dir = new File(baseDir, "blue");
         dir = new File(dir, "fish");
-        dir.mkdirs();
+        org.apache.rat.testhelpers.FileUtils.mkDir(dir);
 
-        try {
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(ImmutablePair.of(option, args));
             DocumentNameMatcher excluder = config.getDocumentExcluder(baseName());
             for (String fname : excluded) {
@@ -245,9 +248,7 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
                 DocumentName docName = mkDocName(fname);
                 assertThat(excluder.matches(docName)).as(() -> dump(option, fname, excluder, docName)).isTrue();
             }
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     private void inputExcludeSizeTest() {
@@ -260,7 +261,7 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
         String[] notExcluded = {"Hello.txt", "HelloWorld.txt"};
         String[] excluded = {"Hi.txt"};
 
-        try {
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(ImmutablePair.of(option, args));
             DocumentNameMatcher excluder = config.getDocumentExcluder(baseName());
             for (String fname : excluded) {
@@ -271,9 +272,7 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
                 DocumentName docName = mkDocName(fname);
                 assertThat(excluder.matches(docName)).as(() -> dump(option, fname, excluder, docName)).isTrue();
             }
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     // include tests
@@ -281,7 +280,8 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
         Option excludeOption = Arg.EXCLUDE.option();
         String[] notExcluded = {"B.bar", "justbaz", "notbaz"};
         String[] excluded = {"some.foo"};
-        try {
+
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(ImmutablePair.of(option, args),
                     ImmutablePair.of(excludeOption, EXCLUDE_ARGS));
             DocumentNameMatcher excluder = config.getDocumentExcluder(baseName());
@@ -293,9 +293,7 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
                 DocumentName docName = mkDocName(fname);
                 assertThat(excluder.matches(docName)).as(() -> dump(option, fname, excluder, docName)).isTrue();
             }
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     private void includeFileTest(final Option option) {
@@ -326,7 +324,8 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
         String[] args = {StandardCollection.MISC.name()};
         String[] excluded = {"afile~more"};
         String[] notExcluded = {"afile~", ".#afile", "%afile%", "._afile", "what.#afile", "%afile%withMore", "well._afile"};
-        try {
+
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(excludes, ImmutablePair.of(option, args));
             DocumentNameMatcher excluder = config.getDocumentExcluder(baseName());
             for (String fname : excluded) {
@@ -337,44 +336,37 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
                 DocumentName docName = mkDocName(fname);
                 assertThat(excluder.matches(docName)).as(() -> dump(option, fname, excluder, docName)).isTrue();
             }
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     protected void inputSourceTest() {
         Option option = Arg.SOURCE.find("input-source");
-        try {
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(ImmutablePair.of(option, new String[]{baseDir.getAbsolutePath()}));
             assertThat(config.hasSource()).isTrue();
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     // LICENSE tests
     protected void execLicensesApprovedTest(final Option option, String[] args) {
         Pair<Option, String[]> arg1 = ImmutablePair.of(option, args);
-        try {
+
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(arg1);
             SortedSet<String> result = config.getLicenseIds(LicenseSetFactory.LicenseFilter.APPROVED);
             assertThat(result).contains("one", "two");
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
 
         Pair<Option, String[]> arg2 = ImmutablePair.of(
                 Arg.CONFIGURATION_NO_DEFAULTS.find("configuration-no-defaults"),
                 null
         );
 
-        try {
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(arg1, arg2);
             SortedSet<String> result = config.getLicenseIds(LicenseSetFactory.LicenseFilter.APPROVED);
             assertThat(result).containsExactly("one", "two");
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     protected void helpLicenses() {
@@ -406,14 +398,12 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
     }
 
     private void execLicensesDeniedTest(final Option option, final String[] args) {
-        try {
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(ImmutablePair.of(option, args));
             assertThat(config.getLicenseIds(LicenseSetFactory.LicenseFilter.ALL)).contains("ILLUMOS");
             SortedSet<String> result = config.getLicenseIds(LicenseSetFactory.LicenseFilter.APPROVED);
             assertThat(result).doesNotContain("ILLUMOS");
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     protected void licensesDeniedTest() {
@@ -429,22 +419,17 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
     private void execLicenseFamiliesApprovedTest(final Option option, final String[] args) {
         String catz = ILicenseFamily.makeCategory("catz");
         Pair<Option, String[]> arg1 = ImmutablePair.of(option, args);
-        try {
+
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(arg1);
             SortedSet<String> result = config.getLicenseCategories(LicenseSetFactory.LicenseFilter.APPROVED);
             assertThat(result).contains(catz);
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
 
-        Pair<Option, String[]> arg2 = ImmutablePair.of(Arg.CONFIGURATION_NO_DEFAULTS.find("configuration-no-defaults"), null);
-        try {
-            ReportConfiguration config = generateConfig(arg1, arg2);
-            SortedSet<String> result = config.getLicenseCategories(LicenseSetFactory.LicenseFilter.APPROVED);
+            Pair<Option, String[]> arg2 = ImmutablePair.of(Arg.CONFIGURATION_NO_DEFAULTS.find("configuration-no-defaults"), null);
+            config = generateConfig(arg1, arg2);
+            result = config.getLicenseCategories(LicenseSetFactory.LicenseFilter.APPROVED);
             assertThat(result).containsExactly(catz);
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     protected void licenseFamiliesApprovedFileTest() {
@@ -460,14 +445,13 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
 
     private void execLicenseFamiliesDeniedTest(final Option option, final String[] args) {
         String gpl = ILicenseFamily.makeCategory("GPL");
-        try {
+
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(ImmutablePair.of(option, args));
             assertThat(config.getLicenseCategories(LicenseSetFactory.LicenseFilter.ALL)).contains(gpl);
             SortedSet<String> result = config.getLicenseCategories(LicenseSetFactory.LicenseFilter.APPROVED);
             assertThat(result).doesNotContain(gpl);
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     protected void licenseFamiliesDeniedFileTest() {
@@ -485,7 +469,7 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
         Option option = Arg.COUNTER_MAX.option();
         String[] args = {null, null};
 
-        try {
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(ImmutablePair.nullPair());
             assertThat(config.getClaimValidator().getMax(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0);
             args[0] = "Unapproved:-1";
@@ -498,16 +482,14 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
             config = generateConfig(ImmutablePair.of(option, args));
             assertThat(config.getClaimValidator().getMax(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(5);
             assertThat(config.getClaimValidator().getMax(ClaimStatistic.Counter.IGNORED)).isEqualTo(0);
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     protected void counterMinTest() {
         Option option = Arg.COUNTER_MIN.option();
         String[] args = {null, null};
 
-        try {
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(ImmutablePair.nullPair());
             assertThat(config.getClaimValidator().getMin(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0);
             args[0] = "Unapproved:1";
@@ -520,15 +502,61 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
             config = generateConfig(ImmutablePair.of(option, args));
             assertThat(config.getClaimValidator().getMin(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(5);
             assertThat(config.getClaimValidator().getMin(ClaimStatistic.Counter.IGNORED)).isEqualTo(0);
+        });
+    }
+
+    private String writeConfigXML(String name) {
+        String licenseText = """
+                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.""";
+        String capName = WordUtils.capitalize(name);
+        String upperName = name.toUpperCase(Locale.ROOT);
+
+        Path xmlPath = this.baseDir.toPath().resolve(".rat/" + capName + ".xml");
+        File f = xmlPath.toFile();
+        try {
+            FileUtils.forceMkdir(f.getParentFile());
+            try (FileWriter writer = new FileWriter(f);
+                 XmlWriter xmlWriter = new XmlWriter(writer)) {
+
+                xmlWriter.startDocument()
+                        .comment(licenseText)
+                        .startElement("rat-config")
+                        .startElement("families")
+                        .startElement("family")
+                        .attribute("id", upperName)
+                        .attribute("name", "from " + capName + ".xml")
+                        .closeElement("families")
+                        .startElement("licenses")
+                        .startElement("license")
+                        .attribute("family", upperName)
+                        .startElement("text")
+                        .content(name)
+                        .closeElement("rat-config");
+            }
         } catch (IOException e) {
-            fail(e.getMessage());
+            throw new RuntimeException(e);
         }
+        return xmlPath.toString();
     }
 
     private void configTest(final Option option) {
-        String[] args = {"src/test/resources/OptionTools/One.xml", "src/test/resources/OptionTools/Two.xml"};
+        String[] args = {writeConfigXML("one"), writeConfigXML("two")};
         Pair<Option, String[]> arg1 = ImmutablePair.of(option, args);
-        try {
+
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(arg1);
             SortedSet<ILicense> set = config.getLicenses(LicenseSetFactory.LicenseFilter.ALL);
             assertThat(set).hasSizeGreaterThan(2);
@@ -542,9 +570,7 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
             assertThat(set).hasSize(2);
             assertThat(LicenseSetFactory.search("ONE", "ONE", set)).isPresent();
             assertThat(LicenseSetFactory.search("TWO", "TWO", set)).isPresent();
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     protected void licensesTest() {
@@ -556,14 +582,12 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
     }
 
     private void noDefaultsTest(final Option arg) {
-        try {
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(ImmutablePair.of(arg, null));
             assertThat(config.getLicenses(LicenseSetFactory.LicenseFilter.ALL)).isEmpty();
             config = generateConfig(ImmutablePair.nullPair());
             assertThat(config.getLicenses(LicenseSetFactory.LicenseFilter.ALL)).isNotEmpty();
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     protected void noDefaultsTest() {
@@ -575,31 +599,39 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
     }
 
     protected void dryRunTest() {
-        try {
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(ImmutablePair.of(Arg.DRY_RUN.find("dry-run"), null));
             assertThat(config.isDryRun()).isTrue();
             config = generateConfig(ImmutablePair.nullPair());
             assertThat(config.isDryRun()).isFalse();
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     private void editCopyrightTest(final Option option) {
-        try {
+        assertDoesNotThrow(() -> {
             Pair<Option, String[]> arg1 = ImmutablePair.of(option, new String[]{"MyCopyright"});
-            ReportConfiguration config = generateConfig(arg1);
-            assertThat(config.getCopyrightMessage()).as("Copyright without --edit-license should not work").isNull();
+            ReportConfiguration config;
+            try {
+                config = generateConfig(arg1);
+                assertThat(config.getCopyrightMessage()).as("Copyright without --edit-license should not work").isNull();
+            } catch (IOException e) {
+                if (e.getCause() != null) {
+                    fail(e.getMessage() + ": " + e.getCause().getMessage());
+                }
+                throw e;
+            }
+
             Pair<Option, String[]> arg2 = ImmutablePair.of(Arg.EDIT_ADD.find("edit-license"), null);
-            config = generateConfig(arg1, arg2);
-            assertThat(config.getCopyrightMessage()).isEqualTo("MyCopyright");
-        } catch (IOException e) {
-            e.printStackTrace();
-            if (e.getCause() != null) {
-                fail(e.getMessage() + ": " + e.getCause().getMessage());
+            try {
+                config = generateConfig(arg1, arg2);
+                assertThat(config.getCopyrightMessage()).isEqualTo("MyCopyright");
+            } catch (IOException e) {
+                if (e.getCause() != null) {
+                    fail(e.getMessage() + ": " + e.getCause().getMessage());
+                }
+                throw e;
             }
-            fail(e.getMessage());
-        }
+        });
     }
 
     protected void copyrightTest() {
@@ -611,14 +643,12 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
     }
 
     private void editLicenseTest(final Option option) {
-        try {
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(ImmutablePair.of(option, null));
             assertThat(config.isAddingLicenses()).isTrue();
             config = generateConfig(ImmutablePair.nullPair());
             assertThat(config.isAddingLicenses()).isFalse();
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     protected void addLicenseTest() {
@@ -631,16 +661,15 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
 
     private void overwriteTest(final Option option) {
         Pair<Option, String[]> arg1 = ImmutablePair.of(option, null);
-        try {
+
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(arg1);
             assertThat(config.isAddingLicensesForced()).isFalse();
             Pair<Option, String[]> arg2 = ImmutablePair.of(Arg.EDIT_ADD.find("edit-license"), null);
 
             config = generateConfig(arg1, arg2);
             assertThat(config.isAddingLicensesForced()).isTrue();
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     protected void forceTest() {
@@ -672,15 +701,14 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
 
     private void archiveTest(final Option option) {
         String[] args = {null};
-        try {
+
+        assertDoesNotThrow(() -> {
             for (ReportConfiguration.Processing proc : ReportConfiguration.Processing.values()) {
                 args[0] = proc.name();
                 ReportConfiguration config = generateConfig(ImmutablePair.of(option, args));
                 assertThat(config.getArchiveProcessing()).isEqualTo(proc);
             }
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     protected void outputArchiveTest() {
@@ -689,15 +717,14 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
 
     private void listFamilies(final Option option) {
         String[] args = {null};
-        for (LicenseSetFactory.LicenseFilter filter : LicenseSetFactory.LicenseFilter.values()) {
-            try {
+
+        assertDoesNotThrow(() -> {
+            for (LicenseSetFactory.LicenseFilter filter : LicenseSetFactory.LicenseFilter.values()) {
                 args[0] = filter.name();
                 ReportConfiguration config = generateConfig(ImmutablePair.of(option, args));
                 assertThat(config.listFamilies()).isEqualTo(filter);
-            } catch (IOException e) {
-                fail(e.getMessage());
             }
-        }
+        });
     }
 
     protected void listFamiliesTest() {
@@ -711,7 +738,8 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
     private void outTest(final Option option) {
         File outFile = new File(baseDir, "outexample-" + option.getLongOpt());
         String[] args = new String[]{outFile.getAbsolutePath()};
-        try {
+
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(ImmutablePair.of(option, args));
             try (OutputStream os = config.getOutput().get()) {
                 os.write("Hello world".getBytes());
@@ -723,9 +751,7 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
             } catch (IOException e) {
                 throw new RuntimeException(e);
             }
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     protected void outTest() {
@@ -738,15 +764,14 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
 
     private void listLicenses(final Option option) {
         String[] args = {null};
-        for (LicenseSetFactory.LicenseFilter filter : LicenseSetFactory.LicenseFilter.values()) {
-            try {
+
+        assertDoesNotThrow(() -> {
+            for (LicenseSetFactory.LicenseFilter filter : LicenseSetFactory.LicenseFilter.values()) {
                 args[0] = filter.name();
                 ReportConfiguration config = generateConfig(ImmutablePair.of(option, args));
                 assertThat(config.listLicenses()).isEqualTo(filter);
-            } catch (IOException e) {
-                fail(e.getMessage());
             }
-        }
+        });
     }
 
     protected void listLicensesTest() {
@@ -759,15 +784,13 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
 
     private void standardTest(final Option option) {
         String[] args = {null};
-        try {
+        assertDoesNotThrow(() -> {
             for (ReportConfiguration.Processing proc : ReportConfiguration.Processing.values()) {
                 args[0] = proc.name();
                 ReportConfiguration config = generateConfig(ImmutablePair.of(option, args));
                 assertThat(config.getStandardProcessing()).isEqualTo(proc);
             }
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     protected void outputStandardTest() {
@@ -780,13 +803,18 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
         try (
                 InputStream in = ReporterTest.class.getResourceAsStream("MatcherContainerResource.txt");
                 OutputStream out = Files.newOutputStream(file.toPath())) {
-            IOUtils.copy(in, out);
+            if (in == null) {
+                fail("Could not copy MatcherContainerResource.txt: resource not found");
+            } else {
+                IOUtils.copy(in, out);
+            }
         } catch (IOException e) {
             fail("Could not copy MatcherContainerResource.txt: " + e.getMessage());
         }
+
         // run the test
         String[] args = {null};
-        try {
+        assertDoesNotThrow(() -> {
             for (String sheet : new String[]{"plain-rat", "missing-headers", "unapproved-licenses", file.getAbsolutePath()}) {
                 args[0] = sheet;
                 ReportConfiguration config = generateConfig(ImmutablePair.of(option, args));
@@ -795,9 +823,7 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
                     assertThat(IOUtils.contentEquals(expected, actual)).as(() -> String.format("'%s' does not match", sheet)).isTrue();
                 }
             }
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     protected void styleSheetTest() {
@@ -809,24 +835,20 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
     }
 
     protected void scanHiddenDirectoriesTest() {
-        try {
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(ImmutablePair.of(Arg.INCLUDE_STD.find("scan-hidden-directories"), null));
             DocumentNameMatcher excluder = config.getDocumentExcluder(baseName());
             assertThat(excluder.matches(mkDocName(".file"))).as(".file").isTrue();
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 
     protected void xmlTest() {
-        try {
+        assertDoesNotThrow(() -> {
             ReportConfiguration config = generateConfig(ImmutablePair.of(Arg.OUTPUT_STYLE.find("xml"), null));
             try (InputStream expected = StyleSheets.getStyleSheet("xml").get();
                  InputStream actual = config.getStyleSheet().get()) {
                 assertThat(IOUtils.contentEquals(expected, actual)).as("'xml' does not match").isTrue();
             }
-        } catch (IOException e) {
-            fail(e.getMessage());
-        }
+        });
     }
 }
diff --git a/apache-rat-tools/src/main/java/org/apache/rat/documentation/options/AntOptionCollection.java b/apache-rat-tools/src/main/java/org/apache/rat/documentation/options/AntOptionCollection.java
index 8b8182ba..88f38c0c 100644
--- a/apache-rat-tools/src/main/java/org/apache/rat/documentation/options/AntOptionCollection.java
+++ b/apache-rat-tools/src/main/java/org/apache/rat/documentation/options/AntOptionCollection.java
@@ -34,6 +34,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.text.WordUtils;
 import org.apache.rat.OptionCollection;
 import org.apache.rat.commandline.Arg;
+import org.apache.rat.document.DocumentName;
 import org.apache.rat.ui.ArgumentTracker;
 import org.apache.rat.ui.UIOptionCollection;
 import org.apache.rat.utils.CasedString;
@@ -49,7 +50,7 @@ public final class AntOptionCollection extends UIOptionCollection<AntOption> {
     private static final Map<String, String> RENAME_MAP;
 
     /**
-     * The format for an XML element
+     * The format for an XML element.
      */
     private static final String DEFAULT_XML = "<%1$s>%%s</%1$s>%n";
 
@@ -78,6 +79,7 @@ public final class AntOptionCollection extends UIOptionCollection<AntOption> {
         ATTRIBUTE_TYPES.add(Integer.class);
         ATTRIBUTE_TYPES.add(Long.class);
         ATTRIBUTE_TYPES.add(File.class);
+        ATTRIBUTE_TYPES.add(DocumentName.class);
     }
 
     public static Map<String, String> getRenameMap() {
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 33009c05..59d2f609 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -68,6 +68,9 @@ in order to be properly linked in site reports.
     </release>
     -->
     <release version="1.0.0-SNAPSHOT" date="xxxx-yy-zz" description="Current SNAPSHOT - release to be done">
+      <action issue="RAT-559" type="fix" dev="claudenw">
+        Fix issues with DocumentName and DocumentNameBuilder that address errors in archive handling and Windows as operating system.
+      </action>
       <action issue="RAT-561" type="add" dev="claudenw">
         Remove IXmlWriter and simplify RatReport XML writer implementations while ensuring all resources are closed in tests.
       </action>