You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by vy...@apache.org on 2023/01/25 16:17:33 UTC

[logging-log4j-tools] 04/07: Add tests for `MavenChangesImporter`

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

vy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/logging-log4j-tools.git

commit 20e56e8123455f932e9ba8902b1b203247fb252c
Author: Volkan Yazıcı <vo...@yazi.ci>
AuthorDate: Wed Jan 25 15:08:49 2023 +0100

    Add tests for `MavenChangesImporter`
---
 log4j-changelog/README.adoc                        | 15 ++++-
 log4j-changelog/pom.xml                            | 12 ++++
 .../logging/log4j/changelog/ChangelogEntry.java    |  4 +-
 .../logging/log4j/changelog/ChangelogRelease.java  |  4 +-
 .../changelog/importer/MavenChangesImporter.java   | 13 +++-
 .../importer/MavenChangesImporterArgs.java         | 21 +++++-
 .../logging/log4j/changelog/util/StringUtils.java  | 13 ++++
 .../logging/log4j/changelog/util/XmlUtils.java     | 42 +++++++++++-
 .../logging/log4j/changelog/util/XmlWriter.java    | 68 ++++++++++++-------
 .../logging/log4j/changelog/FileTestUtils.java     | 75 +++++++++++++++++++++
 .../log4j/changelog/MavenChangesImporterTest.java  | 43 ++++++++++++
 log4j-changelog/src/test/resources/1-changes.xml   | 78 ++++++++++++++++++++++
 ...Add_getExplicitLevel_method_to_LoggerConfig.xml | 25 +++++++
 ...ConsoleListener_use_SimpleLogger_internally.xml | 25 +++++++
 ..._InstantFormatter_against_delegate_failures.xml | 26 ++++++++
 .../test/resources/2-imported/2.17.2/.release.xml} | 33 +--------
 ...s_initialized_if_the_LoggerFactory_is_provi.xml | 26 ++++++++
 ...ContextDataInjector_initialization_deadlock.xml | 25 +++++++
 ..._Spring_Boot_Lookup_requires_the_log4j_spri.xml | 25 +++++++
 .../test/resources/2-imported/2.18.0/.release.xml} | 33 +--------
 ...erStrategy_should_use_the_current_time_when.xml | 25 +++++++
 ...lloverStrategy_was_not_detecting_the_correc.xml | 25 +++++++
 ...se_Paths_get_to_avoid_circular_file_systems.xml | 25 +++++++
 log4j-tools-parent/pom.xml                         | 14 ++++
 24 files changed, 597 insertions(+), 98 deletions(-)

diff --git a/log4j-changelog/README.adoc b/log4j-changelog/README.adoc
index 1e13468..9e30d70 100644
--- a/log4j-changelog/README.adoc
+++ b/log4j-changelog/README.adoc
@@ -89,7 +89,10 @@ A typical `.release.xml` looks as follows:
 ----
 $ cat src/changelog/2.19.0/release.xml
 <?xml version="1.0" encoding="UTF-8"?>
-<release date="2022-09-09" version="2.19.0"/>
+<release xmlns="http://logging.apache.org/log4j/changelog"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.0.xsd"
+         date="2022-09-09" version="2.19.0"/>
 ----
 
 A typical changelog entry file looks as follows:
@@ -98,7 +101,10 @@ A typical changelog entry file looks as follows:
 ----
 $ cat src/changelog/.2.x.x/LOG4J2-3628_new_changelog_infra.xml
 <?xml version="1.0" encoding="UTF-8"?>
-<entry type="fixed">
+<entry xmlns="http://logging.apache.org/log4j/changelog"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.0.xsd"
+       type="fixed">
   <issue id="LOG4J2-3556" link="https://issues.apache.org/jira/browse/LOG4J2-3556"/>
   <author id="vy"/>
   <author name="Arthur Gavlyukovskiy"/>
@@ -161,7 +167,10 @@ A sample _changelog entry_ file is shared below.
 .`src/changelog/LOG4J2-3556_JsonTemplateLayout_stack_trace_truncation_fix.xml` file contents
 [source,xml]
 ----
-<entry type="fixed">
+<entry xmlns="http://logging.apache.org/log4j/changelog"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.0.xsd"
+       type="fixed">
   <issue id="LOG4J2-3556" link="https://issues.apache.org/jira/browse/LOG4J2-3556"/>
   <author id="vy"/>
   <author name="Arthur Gavlyukovskiy"/>
diff --git a/log4j-changelog/pom.xml b/log4j-changelog/pom.xml
index 6e336be..53f0d77 100644
--- a/log4j-changelog/pom.xml
+++ b/log4j-changelog/pom.xml
@@ -43,6 +43,18 @@
       <artifactId>freemarker</artifactId>
     </dependency>
 
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-engine</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.assertj</groupId>
+      <artifactId>assertj-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+
   </dependencies>
 
 </project>
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/ChangelogEntry.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/ChangelogEntry.java
index f31d36a..88a42f2 100644
--- a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/ChangelogEntry.java
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/ChangelogEntry.java
@@ -112,12 +112,10 @@ public final class ChangelogEntry {
     }
 
     public void writeToXmlFile(final Path path) {
-        XmlWriter.toFile(path, document -> {
+        XmlWriter.toFile(path, "entry", (document, entryElement) -> {
 
             // Create the `entry` root element
-            final Element entryElement = document.createElement("entry");
             entryElement.setAttribute("type", type.toXmlAttribute());
-            document.appendChild(entryElement);
 
             // Create the `issue` elements
             issues.forEach(issue -> {
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/ChangelogRelease.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/ChangelogRelease.java
index 2369ba7..b33765b 100644
--- a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/ChangelogRelease.java
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/ChangelogRelease.java
@@ -35,11 +35,9 @@ public final class ChangelogRelease {
     }
 
     public void writeToXmlFile(final Path path) {
-        XmlWriter.toFile(path, document -> {
-            final Element releaseElement = document.createElement("release");
+        XmlWriter.toFile(path, "release", (document, releaseElement) -> {
             releaseElement.setAttribute("version", version);
             releaseElement.setAttribute("date", date);
-            document.appendChild(releaseElement);
         });
     }
 
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/importer/MavenChangesImporter.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/importer/MavenChangesImporter.java
index 318f0c9..33074f8 100644
--- a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/importer/MavenChangesImporter.java
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/importer/MavenChangesImporter.java
@@ -32,6 +32,10 @@ public final class MavenChangesImporter {
 
     public static void main(final String[] mainArgs) {
         final MavenChangesImporterArgs args = MavenChangesImporterArgs.fromSystemProperties();
+        performImport(args);
+    }
+
+    public static void performImport(final MavenChangesImporterArgs args) {
         final MavenChanges mavenChanges = MavenChanges.readFromFile(args.changesXmlFile);
         mavenChanges.releases.forEach(release -> {
             if ("TBD".equals(release.date)) {
@@ -81,11 +85,14 @@ public final class MavenChangesImporter {
         }
         final String sanitizedDescription = action
                 .description
-                .substring(0, Math.min(action.description.length(), 60))
                 .replaceAll("[^A-Za-z0-9]", "_")
                 .replaceAll("_+", "_")
-                .replaceAll("[^A-Za-z0-9]$", "");
-        actionRelativeFileBuilder.append(sanitizedDescription);
+                .replaceAll("^[^A-Za-z0-9]*", "")
+                .replaceAll("[^A-Za-z0-9]*$", "");
+        final String shortenedSanitizedDescription = sanitizedDescription.length() > 60
+                ? sanitizedDescription.substring(0, 60)
+                : sanitizedDescription;
+        actionRelativeFileBuilder.append(shortenedSanitizedDescription);
         actionRelativeFileBuilder.append(".xml");
         return actionRelativeFileBuilder.toString();
     }
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/importer/MavenChangesImporterArgs.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/importer/MavenChangesImporterArgs.java
index fd9f631..515180b 100644
--- a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/importer/MavenChangesImporterArgs.java
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/importer/MavenChangesImporterArgs.java
@@ -17,11 +17,12 @@
 package org.apache.logging.log4j.changelog.importer;
 
 import java.nio.file.Path;
+import java.util.Objects;
 
 import static org.apache.logging.log4j.changelog.util.PropertyUtils.requireNonBlankIntProperty;
 import static org.apache.logging.log4j.changelog.util.PropertyUtils.requireNonBlankPathProperty;
 
-final class MavenChangesImporterArgs {
+public final class MavenChangesImporterArgs {
 
     public static final String CHANGELOG_DIRECTORY_PROPERTY_NAME = "log4j.changelog.directory";
 
@@ -35,10 +36,26 @@ final class MavenChangesImporterArgs {
 
     final int releaseVersionMajor;
 
-    private MavenChangesImporterArgs(final Path changelogDirectory, final Path changesXmlFile, final int releaseVersionMajor) {
+    public MavenChangesImporterArgs(
+            final Path changelogDirectory,
+            final Path changesXmlFile,
+            final int releaseVersionMajor) {
+
+        // Check arguments
+        Objects.requireNonNull(changelogDirectory, "changelogDirectory");
+        Objects.requireNonNull(changesXmlFile, "changesXmlFile");
+        if (releaseVersionMajor < 0) {
+            final String message = String.format(
+                    "was expecting `releaseVersionMajor >= 0`, found: %d",
+                    releaseVersionMajor);
+            throw new IllegalArgumentException(message);
+        }
+
+        // Set fields
         this.changelogDirectory = changelogDirectory;
         this.changesXmlFile = changesXmlFile;
         this.releaseVersionMajor = releaseVersionMajor;
+
     }
 
     static MavenChangesImporterArgs fromSystemProperties() {
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/util/StringUtils.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/util/StringUtils.java
index 1a084c2..ea5953a 100644
--- a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/util/StringUtils.java
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/util/StringUtils.java
@@ -31,4 +31,17 @@ public final class StringUtils {
         return input == null || input.matches("\\s*");
     }
 
+    public static String repeat(final String input, final int count) {
+        if (count < 0) {
+            final String message = String.format("was expecting `count >= 0`, found: %d", count);
+            throw new IllegalArgumentException(message);
+        }
+        final int length = Math.multiplyExact(input.length(), count);
+        final StringBuilder stringBuilder = new StringBuilder(length);
+        for (int i = 0; i < count; i++) {
+            stringBuilder.append(input);
+        }
+        return stringBuilder.toString();
+    }
+
 }
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/util/XmlUtils.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/util/XmlUtils.java
index 0a9ad6a..a438442 100644
--- a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/util/XmlUtils.java
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/util/XmlUtils.java
@@ -16,18 +16,36 @@
  */
 package org.apache.logging.log4j.changelog.util;
 
+import javax.xml.XMLConstants;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+
+import java.io.InputStream;
 
 final class XmlUtils {
 
+    static final String XML_NAMESPACE = "http://logging.apache.org/log4j/changelog";
+
+    static final String XML_SCHEMA_LOCATION = "https://logging.apache.org/log4j/changelog-0.1.0.xsd";
+
     private XmlUtils() {}
 
+    static DocumentBuilderFactory createDocumentBuilderFactory() {
+        final DocumentBuilderFactory dbf = createSecureDocumentBuilderFactory();
+        final Schema schema = readSchema();
+        dbf.setSchema(schema);
+        dbf.setValidating(true);
+        return dbf;
+    }
+
     /**
      * @return a {@link DocumentBuilderFactory} instance configured with certain XXE protection measures
      * @see <a href="https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#jaxp-documentbuilderfactory-saxparserfactory-and-dom4j">XML External Entity Prevention Cheat Sheet</a>
      */
-    static DocumentBuilderFactory createDocumentBuilderFactory() {
+    private static DocumentBuilderFactory createSecureDocumentBuilderFactory() {
         final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
         String feature = null;
         try {
@@ -68,4 +86,26 @@ final class XmlUtils {
         return dbf;
     }
 
+    private static Schema readSchema() {
+
+        // Read the schema file resource
+        final String schemaFileName = "/log4j-changelog.xsd";
+        final InputStream schemaInputStream = XmlUtils.class.getResourceAsStream(schemaFileName);
+        if (schemaInputStream == null) {
+            final String message = String.format("could not find the schema file resource: `%s`", schemaFileName);
+            throw new RuntimeException(message);
+        }
+
+        // Read the schema
+        try {
+            final StreamSource schemaSource = new StreamSource(schemaInputStream);
+            final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+            return schemaFactory.newSchema(schemaSource);
+        } catch (final Exception error) {
+            final String message = String.format("failed to load schema from file resource: `%s`", schemaFileName);
+            throw new RuntimeException(message, error);
+        }
+
+    }
+
 }
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/util/XmlWriter.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/util/XmlWriter.java
index 7577ed5..3af34c4 100644
--- a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/util/XmlWriter.java
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/util/XmlWriter.java
@@ -20,7 +20,7 @@ import java.io.StringWriter;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
-import java.util.function.Consumer;
+import java.util.function.BiConsumer;
 
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
@@ -34,14 +34,18 @@ import edu.umd.cs.findbugs.annotations.Nullable;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import org.w3c.dom.Comment;
 import org.w3c.dom.Document;
+import org.w3c.dom.Element;
 
 public final class XmlWriter {
 
     private XmlWriter() {}
 
-    public static void toFile(final Path filepath, final Consumer<Document> documentConsumer) {
+    public static void toFile(
+            final Path filepath,
+            final String rootElementName,
+            final BiConsumer<Document, Element> documentConsumer) {
         try {
-            final String xml = toString(documentConsumer);
+            final String xml = toString(rootElementName, documentConsumer);
             final byte[] xmlBytes = xml.getBytes(CharsetUtils.CHARSET);
             @Nullable
             final Path filepathParent = filepath.getParent();
@@ -55,7 +59,7 @@ public final class XmlWriter {
         }
     }
 
-    public static String toString(final Consumer<Document> documentConsumer) {
+    public static String toString(final String rootElementName, final BiConsumer<Document, Element> documentConsumer) {
         try {
 
             // Create the document
@@ -66,28 +70,35 @@ public final class XmlWriter {
             final Document document = documentBuilder.newDocument();
             document.setXmlStandalone(true);
             final Comment licenseComment = document.createComment("\n" +
-                    "   Licensed to the Apache Software Foundation (ASF) under one or more\n" +
-                    "   contributor license agreements.  See the NOTICE file distributed with\n" +
-                    "   this work for additional information regarding copyright ownership.\n" +
-                    "   The ASF licenses this file to You under the Apache License, Version 2.0\n" +
-                    "   (the \"License\"); you may not use this file except in compliance with\n" +
-                    "   the License.  You may obtain a copy of the License at\n" +
+                    "  Licensed to the Apache Software Foundation (ASF) under one or more\n" +
+                    "  contributor license agreements. See the NOTICE file distributed with\n" +
+                    "  this work for additional information regarding copyright ownership.\n" +
+                    "  The ASF licenses this file to You under the Apache License, Version 2.0\n" +
+                    "  (the \"License\"); you may not use this file except in compliance with\n" +
+                    "  the License. You may obtain a copy of the License at\n" +
                     "\n" +
-                    "       http://www.apache.org/licenses/LICENSE-2.0\n" +
+                    "      https://www.apache.org/licenses/LICENSE-2.0\n" +
                     "\n" +
-                    "   Unless required by applicable law or agreed to in writing, software\n" +
-                    "   distributed under the License is distributed on an \"AS IS\" BASIS,\n" +
-                    "   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
-                    "   See the License for the specific language governing permissions and\n" +
-                    "   limitations under the License." +
+                    "  Unless required by applicable law or agreed to in writing, software\n" +
+                    "  distributed under the License is distributed on an \"AS IS\" BASIS,\n" +
+                    "  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
+                    "  See the License for the specific language governing permissions and\n" +
+                    "  limitations under the License." +
                     "\n");
             document.appendChild(licenseComment);
 
-            // Execute request changes
-            documentConsumer.accept(document);
+            // Create the root element
+            final Element rootElement = document.createElement(rootElementName);
+            document.appendChild(rootElement);
+
+            // Apply requested changes
+            documentConsumer.accept(document, rootElement);
+//            rootElement.setAttribute("xmlns", XmlUtils.XML_NAMESPACE);
+//            rootElement.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
+//            rootElement.setAttribute("xsi:schemaLocation", XmlUtils.XML_NAMESPACE + " "+ XmlUtils.XML_SCHEMA_LOCATION);
 
             // Serialize the document
-            return serializeXmlDocument(document);
+            return serializeXmlDocument(document, rootElementName);
 
         } catch (final Exception error) {
             throw new RuntimeException("failed writing XML", error);
@@ -95,7 +106,8 @@ public final class XmlWriter {
     }
 
     @SuppressFBWarnings({"XXE_DTD_TRANSFORM_FACTORY", "XXE_XSLT_TRANSFORM_FACTORY"})
-    private static String serializeXmlDocument(final Document document) throws Exception {
+    private static String serializeXmlDocument(final Document document, final String rootElementName) throws Exception {
+
         final Transformer transformer = TransformerFactory.newInstance().newTransformer();
         final StreamResult result = new StreamResult(new StringWriter());
         final DOMSource source = new DOMSource(document);
@@ -103,10 +115,20 @@ public final class XmlWriter {
         transformer.setOutputProperty(OutputKeys.INDENT, "yes");
         transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
         transformer.transform(source, result);
-        return result.getWriter().toString()
-                // Life is too short to solve DOM transformer issues decently
+
+        // Life is too short to solve DOM transformer issues decently
+        final String xml = result.getWriter().toString();
+        final String padding = StringUtils.repeat(" ", rootElementName.length() + 2);
+        return xml
                 .replace("?><!--", "?>\n<!--")
-                .replace("--><", "-->\n<");
+                .replace("--><", "-->\n<")
+                .replaceFirst(
+                        '<' + rootElementName + " (.+>\n)",
+                        ('<' + rootElementName + " xmlns=\"" + XmlUtils.XML_NAMESPACE + "\"\n" +
+                                padding + "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
+                                padding + "xsi:schemaLocation=\"" + XmlUtils.XML_NAMESPACE + " "+ XmlUtils.XML_SCHEMA_LOCATION + "\"\n" +
+                                padding + "$1"));
+
     }
 
 }
diff --git a/log4j-changelog/src/test/java/org/apache/logging/log4j/changelog/FileTestUtils.java b/log4j-changelog/src/test/java/org/apache/logging/log4j/changelog/FileTestUtils.java
new file mode 100644
index 0000000..d67ff40
--- /dev/null
+++ b/log4j-changelog/src/test/java/org/apache/logging/log4j/changelog/FileTestUtils.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.changelog;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+final class FileTestUtils {
+
+    private FileTestUtils() {}
+
+    static void assertDirectoryContentMatches(final Path actualPath, final Path expectedPath) {
+
+        // Compare file paths
+        final Map<String, Path> actualContents = directoryContents(actualPath);
+        final Map<String, Path> expectedContents = directoryContents(expectedPath);
+        final Set<String> relativeFilePaths = expectedContents.keySet();
+        assertThat(actualContents).containsOnlyKeys(relativeFilePaths);
+
+        // Compare file contents
+        relativeFilePaths.forEach(relativeFilePath -> {
+            final Path actualFilePath = actualContents.get(relativeFilePath);
+            final Path expectedFilePath = expectedContents.get(relativeFilePath);
+            if (!Files.isDirectory(actualFilePath) || !Files.isDirectory(expectedFilePath)) {
+                assertThat(actualFilePath).hasSameTextualContentAs(expectedFilePath, StandardCharsets.UTF_8);
+            }
+        });
+
+    }
+
+    private static Map<String, Path> directoryContents(final Path root) {
+        final int rootPathLength = root.toAbsolutePath().toString().length();
+        try (final Stream<Path> paths = Files.walk(root)) {
+            return paths
+                    .filter(path -> !root.equals(path))
+                    .collect(Collectors.toMap(
+                            path -> path.toAbsolutePath().toString().substring(rootPathLength + 1),
+                            Function.identity(),
+                            (oldPath, newPath) -> {
+                                final String message = String.format(
+                                        "paths `%s` and `%s` have conflicting keys",
+                                        oldPath, newPath);
+                                throw new IllegalStateException(message);
+                            },
+                            TreeMap::new));
+        } catch (final IOException error) {
+            final String message = String.format("failed walking directory: `%s`", root);
+            throw new UncheckedIOException(message, error);
+        }
+    }
+
+}
diff --git a/log4j-changelog/src/test/java/org/apache/logging/log4j/changelog/MavenChangesImporterTest.java b/log4j-changelog/src/test/java/org/apache/logging/log4j/changelog/MavenChangesImporterTest.java
new file mode 100644
index 0000000..ffaef0d
--- /dev/null
+++ b/log4j-changelog/src/test/java/org/apache/logging/log4j/changelog/MavenChangesImporterTest.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.changelog;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.apache.logging.log4j.changelog.importer.MavenChangesImporter;
+import org.apache.logging.log4j.changelog.importer.MavenChangesImporterArgs;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.CleanupMode;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.apache.logging.log4j.changelog.FileTestUtils.assertDirectoryContentMatches;
+
+class MavenChangesImporterTest {
+
+    @Test
+    void output_should_match(@TempDir(cleanup = CleanupMode.ON_SUCCESS) final Path changelogDirectory) {
+        final MavenChangesImporterArgs args = new MavenChangesImporterArgs(
+                changelogDirectory,
+                Paths.get("src/test/resources/1-changes.xml"),
+                2);
+        MavenChangesImporter.performImport(args);
+        assertDirectoryContentMatches(changelogDirectory, Paths.get("src/test/resources/2-imported"));
+    }
+
+}
diff --git a/log4j-changelog/src/test/resources/1-changes.xml b/log4j-changelog/src/test/resources/1-changes.xml
new file mode 100644
index 0000000..3a17e7c
--- /dev/null
+++ b/log4j-changelog/src/test/resources/1-changes.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<document xmlns="http://maven.apache.org/changes/1.0.0"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/changes/1.0.0 http://maven.apache.org/xsd/changes-1.0.0.xsd">
+
+  <properties>
+    <title>Changes</title>
+  </properties>
+
+  <body>
+
+    <release version="2.19.0" date="TBD" description="GA Release 2.19.0">
+
+      <action issue="LOG4J2-3584" dev="vy" type="fix">
+        Make `StatusConsoleListener` use `SimpleLogger` internally
+      </action>
+
+      <action issue="LOG4J2-3614" dev="vy" type="fix" due-to="strainu">
+        Harden `InstantFormatter` against delegate failures
+      </action>
+
+      <action issue="LOG4J2-3572" dev="rgoers" type="update">
+        Add `getExplicitLevel` method to `LoggerConfig`
+      </action>
+
+    </release>
+
+    <release version="2.18.0" date="2022-06-28" description="GA Release 2.18.0">
+
+      <action issue="LOG4J2-3339" dev="rgoers" type="fix">
+        `DirectWriteRolloverStrategy` should use the current time when creating files
+      </action>
+
+      <action issue="LOG4J2-3527" dev="rgoers" type="add">
+        Don't use `Paths.get()` to avoid circular file systems
+      </action>
+
+      <action issue="LOG4J2-3490" dev="rgoers" type="remove">
+        The `DirectWriteRolloverStrategy` was not detecting the correct index to use during startup
+      </action>
+
+    </release>
+
+    <release version="2.17.2" date="2022-02-23" description="GA Release 2.17.2">
+
+      <action issue="LOG4J2-3304" dev="rgoers" type="fix" due-to="francis-FY">
+        Flag `LogManager` as initialized if the `LoggerFactory` is provided as a property
+      </action>
+
+      <action issue="LOG4J2-3405" dev="rgoers" type="remove">
+        Document that the Spring Boot Lookup requires the `log4j-spring-boot` dependency
+      </action>
+
+      <action issue="LOG4J2-3333" dev="ckozak" type="remove">
+        Fix `ThreadContextDataInjector` initialization deadlock
+      </action>
+
+    </release>
+
+  </body>
+
+</document>
diff --git a/log4j-changelog/src/test/resources/2-imported/.2.x.x/LOG4J2-3572_Add_getExplicitLevel_method_to_LoggerConfig.xml b/log4j-changelog/src/test/resources/2-imported/.2.x.x/LOG4J2-3572_Add_getExplicitLevel_method_to_LoggerConfig.xml
new file mode 100644
index 0000000..b135ac2
--- /dev/null
+++ b/log4j-changelog/src/test/resources/2-imported/.2.x.x/LOG4J2-3572_Add_getExplicitLevel_method_to_LoggerConfig.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<entry xmlns="http://logging.apache.org/log4j/changelog"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.0.xsd"
+       type="changed">
+  <issue id="LOG4J2-3572" link="https://issues.apache.org/jira/browse/LOG4J2-3572"/>
+  <author id="rgoers"/>
+  <description format="asciidoc">Add `getExplicitLevel` method to `LoggerConfig`</description>
+</entry>
diff --git a/log4j-changelog/src/test/resources/2-imported/.2.x.x/LOG4J2-3584_Make_StatusConsoleListener_use_SimpleLogger_internally.xml b/log4j-changelog/src/test/resources/2-imported/.2.x.x/LOG4J2-3584_Make_StatusConsoleListener_use_SimpleLogger_internally.xml
new file mode 100644
index 0000000..7902947
--- /dev/null
+++ b/log4j-changelog/src/test/resources/2-imported/.2.x.x/LOG4J2-3584_Make_StatusConsoleListener_use_SimpleLogger_internally.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<entry xmlns="http://logging.apache.org/log4j/changelog"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.0.xsd"
+       type="fixed">
+  <issue id="LOG4J2-3584" link="https://issues.apache.org/jira/browse/LOG4J2-3584"/>
+  <author id="vy"/>
+  <description format="asciidoc">Make `StatusConsoleListener` use `SimpleLogger` internally</description>
+</entry>
diff --git a/log4j-changelog/src/test/resources/2-imported/.2.x.x/LOG4J2-3614_Harden_InstantFormatter_against_delegate_failures.xml b/log4j-changelog/src/test/resources/2-imported/.2.x.x/LOG4J2-3614_Harden_InstantFormatter_against_delegate_failures.xml
new file mode 100644
index 0000000..34111cf
--- /dev/null
+++ b/log4j-changelog/src/test/resources/2-imported/.2.x.x/LOG4J2-3614_Harden_InstantFormatter_against_delegate_failures.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<entry xmlns="http://logging.apache.org/log4j/changelog"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.0.xsd"
+       type="fixed">
+  <issue id="LOG4J2-3614" link="https://issues.apache.org/jira/browse/LOG4J2-3614"/>
+  <author id="vy"/>
+  <author name="strainu"/>
+  <description format="asciidoc">Harden `InstantFormatter` against delegate failures</description>
+</entry>
diff --git a/log4j-changelog/pom.xml b/log4j-changelog/src/test/resources/2-imported/2.17.2/.release.xml
similarity index 52%
copy from log4j-changelog/pom.xml
copy to log4j-changelog/src/test/resources/2-imported/2.17.2/.release.xml
index 6e336be..b46393f 100644
--- a/log4j-changelog/pom.xml
+++ b/log4j-changelog/src/test/resources/2-imported/2.17.2/.release.xml
@@ -15,34 +15,7 @@
   See the License for the specific language governing permissions and
   limitations under the License.
 -->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
+<release xmlns="http://logging.apache.org/log4j/changelog"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <artifactId>log4j-tools-parent</artifactId>
-    <groupId>org.apache.logging.log4j</groupId>
-    <version>${revision}</version>
-    <relativePath>../log4j-tools-parent/pom.xml</relativePath>
-  </parent>
-
-  <artifactId>log4j-changelog</artifactId>
-
-  <dependencies>
-
-    <dependency>
-      <groupId>com.github.spotbugs</groupId>
-      <artifactId>spotbugs-annotations</artifactId>
-      <scope>provided</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>org.freemarker</groupId>
-      <artifactId>freemarker</artifactId>
-    </dependency>
-
-  </dependencies>
-
-</project>
+         xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.0.xsd"
+         date="2022-02-23" version="2.17.2"/>
diff --git a/log4j-changelog/src/test/resources/2-imported/2.17.2/LOG4J2-3304_Flag_LogManager_as_initialized_if_the_LoggerFactory_is_provi.xml b/log4j-changelog/src/test/resources/2-imported/2.17.2/LOG4J2-3304_Flag_LogManager_as_initialized_if_the_LoggerFactory_is_provi.xml
new file mode 100644
index 0000000..242b03e
--- /dev/null
+++ b/log4j-changelog/src/test/resources/2-imported/2.17.2/LOG4J2-3304_Flag_LogManager_as_initialized_if_the_LoggerFactory_is_provi.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<entry xmlns="http://logging.apache.org/log4j/changelog"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.0.xsd"
+       type="fixed">
+  <issue id="LOG4J2-3304" link="https://issues.apache.org/jira/browse/LOG4J2-3304"/>
+  <author id="rgoers"/>
+  <author name="francis-FY"/>
+  <description format="asciidoc">Flag `LogManager` as initialized if the `LoggerFactory` is provided as a property</description>
+</entry>
diff --git a/log4j-changelog/src/test/resources/2-imported/2.17.2/LOG4J2-3333_Fix_ThreadContextDataInjector_initialization_deadlock.xml b/log4j-changelog/src/test/resources/2-imported/2.17.2/LOG4J2-3333_Fix_ThreadContextDataInjector_initialization_deadlock.xml
new file mode 100644
index 0000000..cb4ea04
--- /dev/null
+++ b/log4j-changelog/src/test/resources/2-imported/2.17.2/LOG4J2-3333_Fix_ThreadContextDataInjector_initialization_deadlock.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<entry xmlns="http://logging.apache.org/log4j/changelog"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.0.xsd"
+       type="removed">
+  <issue id="LOG4J2-3333" link="https://issues.apache.org/jira/browse/LOG4J2-3333"/>
+  <author id="ckozak"/>
+  <description format="asciidoc">Fix `ThreadContextDataInjector` initialization deadlock</description>
+</entry>
diff --git a/log4j-changelog/src/test/resources/2-imported/2.17.2/LOG4J2-3405_Document_that_the_Spring_Boot_Lookup_requires_the_log4j_spri.xml b/log4j-changelog/src/test/resources/2-imported/2.17.2/LOG4J2-3405_Document_that_the_Spring_Boot_Lookup_requires_the_log4j_spri.xml
new file mode 100644
index 0000000..6b213cb
--- /dev/null
+++ b/log4j-changelog/src/test/resources/2-imported/2.17.2/LOG4J2-3405_Document_that_the_Spring_Boot_Lookup_requires_the_log4j_spri.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<entry xmlns="http://logging.apache.org/log4j/changelog"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.0.xsd"
+       type="removed">
+  <issue id="LOG4J2-3405" link="https://issues.apache.org/jira/browse/LOG4J2-3405"/>
+  <author id="rgoers"/>
+  <description format="asciidoc">Document that the Spring Boot Lookup requires the `log4j-spring-boot` dependency</description>
+</entry>
diff --git a/log4j-changelog/pom.xml b/log4j-changelog/src/test/resources/2-imported/2.18.0/.release.xml
similarity index 52%
copy from log4j-changelog/pom.xml
copy to log4j-changelog/src/test/resources/2-imported/2.18.0/.release.xml
index 6e336be..2f7068a 100644
--- a/log4j-changelog/pom.xml
+++ b/log4j-changelog/src/test/resources/2-imported/2.18.0/.release.xml
@@ -15,34 +15,7 @@
   See the License for the specific language governing permissions and
   limitations under the License.
 -->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
+<release xmlns="http://logging.apache.org/log4j/changelog"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <artifactId>log4j-tools-parent</artifactId>
-    <groupId>org.apache.logging.log4j</groupId>
-    <version>${revision}</version>
-    <relativePath>../log4j-tools-parent/pom.xml</relativePath>
-  </parent>
-
-  <artifactId>log4j-changelog</artifactId>
-
-  <dependencies>
-
-    <dependency>
-      <groupId>com.github.spotbugs</groupId>
-      <artifactId>spotbugs-annotations</artifactId>
-      <scope>provided</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>org.freemarker</groupId>
-      <artifactId>freemarker</artifactId>
-    </dependency>
-
-  </dependencies>
-
-</project>
+         xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.0.xsd"
+         date="2022-06-28" version="2.18.0"/>
diff --git a/log4j-changelog/src/test/resources/2-imported/2.18.0/LOG4J2-3339_DirectWriteRolloverStrategy_should_use_the_current_time_when.xml b/log4j-changelog/src/test/resources/2-imported/2.18.0/LOG4J2-3339_DirectWriteRolloverStrategy_should_use_the_current_time_when.xml
new file mode 100644
index 0000000..8490b36
--- /dev/null
+++ b/log4j-changelog/src/test/resources/2-imported/2.18.0/LOG4J2-3339_DirectWriteRolloverStrategy_should_use_the_current_time_when.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<entry xmlns="http://logging.apache.org/log4j/changelog"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.0.xsd"
+       type="fixed">
+  <issue id="LOG4J2-3339" link="https://issues.apache.org/jira/browse/LOG4J2-3339"/>
+  <author id="rgoers"/>
+  <description format="asciidoc">`DirectWriteRolloverStrategy` should use the current time when creating files</description>
+</entry>
diff --git a/log4j-changelog/src/test/resources/2-imported/2.18.0/LOG4J2-3490_The_DirectWriteRolloverStrategy_was_not_detecting_the_correc.xml b/log4j-changelog/src/test/resources/2-imported/2.18.0/LOG4J2-3490_The_DirectWriteRolloverStrategy_was_not_detecting_the_correc.xml
new file mode 100644
index 0000000..c60d17b
--- /dev/null
+++ b/log4j-changelog/src/test/resources/2-imported/2.18.0/LOG4J2-3490_The_DirectWriteRolloverStrategy_was_not_detecting_the_correc.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<entry xmlns="http://logging.apache.org/log4j/changelog"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.0.xsd"
+       type="removed">
+  <issue id="LOG4J2-3490" link="https://issues.apache.org/jira/browse/LOG4J2-3490"/>
+  <author id="rgoers"/>
+  <description format="asciidoc">The `DirectWriteRolloverStrategy` was not detecting the correct index to use during startup</description>
+</entry>
diff --git a/log4j-changelog/src/test/resources/2-imported/2.18.0/LOG4J2-3527_Don_t_use_Paths_get_to_avoid_circular_file_systems.xml b/log4j-changelog/src/test/resources/2-imported/2.18.0/LOG4J2-3527_Don_t_use_Paths_get_to_avoid_circular_file_systems.xml
new file mode 100644
index 0000000..d7e296b
--- /dev/null
+++ b/log4j-changelog/src/test/resources/2-imported/2.18.0/LOG4J2-3527_Don_t_use_Paths_get_to_avoid_circular_file_systems.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<entry xmlns="http://logging.apache.org/log4j/changelog"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.0.xsd"
+       type="added">
+  <issue id="LOG4J2-3527" link="https://issues.apache.org/jira/browse/LOG4J2-3527"/>
+  <author id="rgoers"/>
+  <description format="asciidoc">Don't use `Paths.get()` to avoid circular file systems</description>
+</entry>
diff --git a/log4j-tools-parent/pom.xml b/log4j-tools-parent/pom.xml
index 671e311..c2dbf46 100644
--- a/log4j-tools-parent/pom.xml
+++ b/log4j-tools-parent/pom.xml
@@ -52,7 +52,9 @@
     <maven.site.deploy.skip>true</maven.site.deploy.skip>
 
     <!-- dependency versions -->
+    <assertj.version>3.24.0</assertj.version>
     <freemarker.version>2.3.32</freemarker.version>
+    <junit.version>5.9.2</junit.version>
     <maven-plugin.version>3.7.1</maven-plugin.version>
     <maven-plugin-api.version>3.8.7</maven-plugin-api.version>
     <spotbugs.version>4.7.3</spotbugs.version>
@@ -68,12 +70,24 @@
   <dependencyManagement>
     <dependencies>
 
+      <dependency>
+        <groupId>org.assertj</groupId>
+        <artifactId>assertj-core</artifactId>
+        <version>${assertj.version}</version>
+      </dependency>
+
       <dependency>
         <groupId>org.freemarker</groupId>
         <artifactId>freemarker</artifactId>
         <version>${freemarker.version}</version>
       </dependency>
 
+      <dependency>
+        <groupId>org.junit.jupiter</groupId>
+        <artifactId>junit-jupiter-engine</artifactId>
+        <version>${junit.version}</version>
+      </dependency>
+
       <dependency>
         <groupId>org.apache.maven.plugin-tools</groupId>
         <artifactId>maven-plugin-annotations</artifactId>