diff --git a/src/test/java/au/com/acegi/xmlformat/AbstractXmlPluginMojoRuleTest.java b/src/test/java/au/com/acegi/xmlformat/AbstractXmlPluginMojoRuleTest.java
new file mode 100644
index 0000000..878e1a8
--- /dev/null
+++ b/src/test/java/au/com/acegi/xmlformat/AbstractXmlPluginMojoRuleTest.java
@@ -0,0 +1,334 @@
+/*
+ * XML Format Maven Plugin (https://github.com/acegi/xml-format-maven-plugin)
+ *
+ * Copyright 2011-2026 Acegi Technology Pty Limited.
+ *
+ * Licensed 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 au.com.acegi.xmlformat;
+
+import static au.com.acegi.xmlformat.TestUtil.fileToString;
+import static au.com.acegi.xmlformat.TestUtil.stringToFile;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.testing.MojoRule;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Integration-style tests for {@link AbstractXmlPlugin} subclasses using the Maven Plugin Testing Harness
+ * ({@link MojoRule}).
+ *
+ *
+ * The harness is used specifically for its {@code setVariableValueToObject} utility, which reflects into private
+ * {@code @Parameter} fields that have no package-private setters — such as {@code tabIndent}, {@code lineEnding},
+ * {@code keepBlankLines}, {@code expandEmptyElements}, and {@code indentSize}.
+ *
+ *
+ * Mojos are created directly via {@code new} rather than Plexus container lookup (which requires a pre-generated Sisu
+ * component index). The package-private setters in {@link AbstractXmlPlugin} handle the mandatory parameters while the
+ * harness handles the remainder via reflection.
+ */
+public class AbstractXmlPluginMojoRuleTest {
+
+ /** Exposes the protected {@code before}/{@code after} lifecycle hooks for use in JUnit 5. */
+ private static final class TestMojoRule extends MojoRule {
+ @Override
+ public void before() throws Throwable {
+ super.before();
+ }
+
+ @Override
+ public void after() {
+ super.after();
+ }
+ }
+
+ private static final String UNFORMATTED_XML = " ";
+ private static final String INCLUDE_ALL_XML = "**/*.xml";
+
+ @TempDir
+ private File tmp;
+
+ private final TestMojoRule rule = new TestMojoRule();
+
+ @BeforeEach
+ void setupHarness() throws Throwable {
+ rule.before();
+ }
+
+ @AfterEach
+ void tearDownHarness() {
+ rule.after();
+ }
+
+ // -------------------------------------------------------------------------
+ // Mojo instantiation
+ // -------------------------------------------------------------------------
+
+ @Test
+ void xmlFormatMojoCanBeInstantiated() {
+ assertThat(new XmlFormatPlugin(), notNullValue());
+ }
+
+ @Test
+ void xmlCheckMojoCanBeInstantiated() {
+ assertThat(new XmlCheckPlugin(), notNullValue());
+ }
+
+ // -------------------------------------------------------------------------
+ // xml-format goal — parameter variations via MojoRule reflection
+ // -------------------------------------------------------------------------
+
+ @Test
+ void xmlFormatWithDefaultSettings() throws Exception {
+ final File proj = createProjectDir("default");
+ final File target = createTargetDir(proj);
+ writeXmlFile(proj, "sample.xml", UNFORMATTED_XML);
+
+ final XmlFormatPlugin mojo = configureMojo(new XmlFormatPlugin(), proj, target);
+ mojo.execute();
+
+ final String formatted = fileToString(proj.toPath().resolve("sample.xml").toFile());
+ assertThat(formatted, containsString("\n"));
+ }
+
+ @Test
+ void xmlFormatWithTabIndent() throws Exception {
+ final File proj = createProjectDir("tabindent");
+ final File target = createTargetDir(proj);
+ writeXmlFile(proj, "sample.xml", UNFORMATTED_XML);
+
+ final XmlFormatPlugin mojo = configureMojo(new XmlFormatPlugin(), proj, target);
+ rule.setVariableValueToObject(mojo, "tabIndent", true);
+ mojo.execute();
+
+ final String formatted = fileToString(proj.toPath().resolve("sample.xml").toFile());
+ assertThat(formatted, containsString("\t"));
+ }
+
+ @Test
+ void xmlFormatWithCrlfLineEnding() throws Exception {
+ final File proj = createProjectDir("crlf");
+ final File target = createTargetDir(proj);
+ writeXmlFile(proj, "sample.xml", UNFORMATTED_XML);
+
+ final XmlFormatPlugin mojo = configureMojo(new XmlFormatPlugin(), proj, target);
+ rule.setVariableValueToObject(mojo, "lineEnding", LineEnding.CRLF);
+ mojo.execute();
+
+ final String formatted = fileToString(proj.toPath().resolve("sample.xml").toFile());
+ assertThat(formatted, containsString("\r\n"));
+ }
+
+ @Test
+ void xmlFormatWithCrLineEnding() throws Exception {
+ final File proj = createProjectDir("cr");
+ final File target = createTargetDir(proj);
+ writeXmlFile(proj, "sample.xml", UNFORMATTED_XML);
+
+ final XmlFormatPlugin mojo = configureMojo(new XmlFormatPlugin(), proj, target);
+ rule.setVariableValueToObject(mojo, "lineEnding", LineEnding.CR);
+ mojo.execute();
+
+ final String formatted = fileToString(proj.toPath().resolve("sample.xml").toFile());
+ assertThat(formatted, containsString("\r"));
+ }
+
+ @Test
+ void xmlFormatWithKeepBlankLines() throws Exception {
+ final File proj = createProjectDir("keepblanklines");
+ final File target = createTargetDir(proj);
+ final String xmlWithBlankLine = "\n\n \n\n";
+ writeXmlFile(proj, "sample.xml", xmlWithBlankLine);
+
+ final XmlFormatPlugin mojo = configureMojo(new XmlFormatPlugin(), proj, target);
+ rule.setVariableValueToObject(mojo, "keepBlankLines", true);
+ mojo.execute();
+
+ final String formatted = fileToString(proj.toPath().resolve("sample.xml").toFile());
+ assertThat(formatted, containsString("\n\n"));
+ }
+
+ @Test
+ void xmlFormatWithExpandEmptyElements() throws Exception {
+ final File proj = createProjectDir("expand");
+ final File target = createTargetDir(proj);
+ writeXmlFile(proj, "sample.xml", "");
+
+ final XmlFormatPlugin mojo = configureMojo(new XmlFormatPlugin(), proj, target);
+ rule.setVariableValueToObject(mojo, "expandEmptyElements", true);
+ mojo.execute();
+
+ final String formatted = fileToString(proj.toPath().resolve("sample.xml").toFile());
+ assertThat(formatted, containsString(""));
+ }
+
+ @Test
+ void xmlFormatWithCustomIndentSize() throws Exception {
+ final File proj = createProjectDir("indent4");
+ final File target = createTargetDir(proj);
+ writeXmlFile(proj, "sample.xml", UNFORMATTED_XML);
+
+ final XmlFormatPlugin mojo = configureMojo(new XmlFormatPlugin(), proj, target);
+ rule.setVariableValueToObject(mojo, "indentSize", 4);
+ mojo.execute();
+
+ final String formatted = fileToString(proj.toPath().resolve("sample.xml").toFile());
+ assertThat(formatted, containsString(" T configureMojo(final T mojo, final File baseDir, final File targetDir)
+ throws IllegalAccessException {
+ mojo.setBaseDirectory(baseDir);
+ mojo.setTargetDirectory(targetDir);
+ mojo.setIncludes(INCLUDE_ALL_XML);
+ mojo.setExcludes();
+ // These @Parameter fields have defaultValue != Java field default, so we must set them explicitly.
+ rule.setVariableValueToObject(mojo, "newlines", true);
+ rule.setVariableValueToObject(mojo, "trimText", true);
+ rule.setVariableValueToObject(mojo, "indentSize", 2);
+ return mojo;
+ }
+}
diff --git a/src/test/java/au/com/acegi/xmlformat/FormatUtilTest.java b/src/test/java/au/com/acegi/xmlformat/FormatUtilTest.java
index b2ee125..e956fec 100644
--- a/src/test/java/au/com/acegi/xmlformat/FormatUtilTest.java
+++ b/src/test/java/au/com/acegi/xmlformat/FormatUtilTest.java
@@ -21,6 +21,7 @@
import static au.com.acegi.xmlformat.FormatUtil.format;
import static au.com.acegi.xmlformat.FormatUtil.formatInPlace;
+import static au.com.acegi.xmlformat.FormatUtil.needsFormatting;
import static au.com.acegi.xmlformat.TestUtil.getResource;
import static au.com.acegi.xmlformat.TestUtil.streamToString;
import static au.com.acegi.xmlformat.TestUtil.stringToFile;
@@ -174,6 +175,45 @@ void unformattedWillChange() throws DocumentException, IOException {
inPlaceChange(UNFORMATTED_XML, true);
}
+ @Test
+ void needsFormattingReturnsFalseForEmptyFile() throws DocumentException, IOException {
+ final File file = File.createTempFile("junit", null, tmp);
+ // empty file: length == 0
+ assertThat(needsFormatting(file, new XmlOutputFormat()), is(false));
+ }
+
+ @Test
+ void needsFormattingReturnsFalseForFormattedFile() throws DocumentException, IOException {
+ final File file = File.createTempFile("junit", null, tmp);
+ stringToFile(FORMATTED_XML, file);
+
+ final XmlOutputFormat fmt = new XmlOutputFormat();
+ fmt.setSuppressDeclaration(true);
+ fmt.setIndent("");
+ fmt.setNewlines(false);
+
+ assertThat(needsFormatting(file, fmt), is(false));
+ }
+
+ @Test
+ void needsFormattingReturnsTrueForUnformattedFile() throws DocumentException, IOException {
+ final File file = File.createTempFile("junit", null, tmp);
+ stringToFile(UNFORMATTED_XML, file);
+
+ final XmlOutputFormat fmt = new XmlOutputFormat();
+ fmt.setSuppressDeclaration(true);
+ fmt.setIndent("");
+ fmt.setNewlines(false);
+
+ assertThat(needsFormatting(file, fmt), is(true));
+ }
+
+ @Test
+ void formatInPlaceReturnsFalseForEmptyFile() throws DocumentException, IOException {
+ final File file = File.createTempFile("junit", null, tmp);
+ assertThat(formatInPlace(file, new XmlOutputFormat()), is(false));
+ }
+
private void inPlaceChange(final String txt, final boolean shouldChange) throws DocumentException, IOException {
final File file = File.createTempFile("junit", null, tmp);
stringToFile(txt, file);
diff --git a/src/test/java/au/com/acegi/xmlformat/IOTest.java b/src/test/java/au/com/acegi/xmlformat/IOTest.java
index 2c11f82..1ba9516 100644
--- a/src/test/java/au/com/acegi/xmlformat/IOTest.java
+++ b/src/test/java/au/com/acegi/xmlformat/IOTest.java
@@ -19,19 +19,26 @@
import static au.com.acegi.xmlformat.IOUtil.hash;
import static au.com.acegi.xmlformat.TestUtil.getResource;
+import static au.com.acegi.xmlformat.TestUtil.stringToFile;
import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
/**
* Tests {@link IOUtil}.
*/
public class IOTest {
+ @TempDir
+ private File tmp;
+
@Test
void hash1() throws IOException {
testHash("/test1-in.xml", 459_402_491L);
@@ -47,6 +54,39 @@ void hashInvalid() throws IOException {
testHash("/invalid.xml", 2_274_913_643L);
}
+ @Test
+ void hashFileMatchesStreamHash() throws IOException {
+ final File file = File.createTempFile("junit", ".xml", tmp);
+ stringToFile("", file);
+
+ final long fileHash = hash(file);
+ try (InputStream in = java.nio.file.Files.newInputStream(file.toPath())) {
+ final long streamHash = hash(in);
+ assertThat(fileHash, is(streamHash));
+ }
+ }
+
+ @Test
+ void hashDifferentContentProducesDifferentHash() throws IOException {
+ final File file1 = File.createTempFile("junit-a", ".xml", tmp);
+ final File file2 = File.createTempFile("junit-b", ".xml", tmp);
+ stringToFile("", file1);
+ stringToFile("", file2);
+
+ assertThat(hash(file1), not(hash(file2)));
+ }
+
+ @Test
+ void hashSameContentProducesSameHash() throws IOException {
+ final File file1 = File.createTempFile("junit-c", ".xml", tmp);
+ final File file2 = File.createTempFile("junit-d", ".xml", tmp);
+ final String content = "";
+ stringToFile(content, file1);
+ stringToFile(content, file2);
+
+ assertThat(hash(file1), is(hash(file2)));
+ }
+
private void testHash(final String resource, final long expected) throws IOException {
try (InputStream in = getResource(resource)) {
final long hash = hash(in);
diff --git a/src/test/java/au/com/acegi/xmlformat/LineEndingTest.java b/src/test/java/au/com/acegi/xmlformat/LineEndingTest.java
new file mode 100644
index 0000000..885b168
--- /dev/null
+++ b/src/test/java/au/com/acegi/xmlformat/LineEndingTest.java
@@ -0,0 +1,77 @@
+/*
+ * XML Format Maven Plugin (https://github.com/acegi/xml-format-maven-plugin)
+ *
+ * Copyright 2011-2026 Acegi Technology Pty Limited.
+ *
+ * Licensed 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 au.com.acegi.xmlformat;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link LineEnding}.
+ */
+public class LineEndingTest {
+
+ @Test
+ void systemEndingIsSystemSeparator() {
+ assertThat(LineEnding.SYSTEM.getChars(), is(System.lineSeparator()));
+ }
+
+ @Test
+ void lfEndingIsNewline() {
+ assertThat(LineEnding.LF.getChars(), is("\n"));
+ }
+
+ @Test
+ void crlfEndingIsCarriageReturnNewline() {
+ assertThat(LineEnding.CRLF.getChars(), is("\r\n"));
+ }
+
+ @Test
+ void crEndingIsCarriageReturn() {
+ assertThat(LineEnding.CR.getChars(), is("\r"));
+ }
+
+ @Test
+ void allValuesAreAccessible() {
+ final LineEnding[] values = LineEnding.values();
+ assertThat(values, notNullValue());
+ assertThat(values.length, is(4));
+ }
+
+ @Test
+ void valueOfSystem() {
+ assertThat(LineEnding.valueOf("SYSTEM"), is(LineEnding.SYSTEM));
+ }
+
+ @Test
+ void valueOfLf() {
+ assertThat(LineEnding.valueOf("LF"), is(LineEnding.LF));
+ }
+
+ @Test
+ void valueOfCrlf() {
+ assertThat(LineEnding.valueOf("CRLF"), is(LineEnding.CRLF));
+ }
+
+ @Test
+ void valueOfCr() {
+ assertThat(LineEnding.valueOf("CR"), is(LineEnding.CR));
+ }
+}
diff --git a/src/test/java/au/com/acegi/xmlformat/XmlCheckPluginTest.java b/src/test/java/au/com/acegi/xmlformat/XmlCheckPluginTest.java
index c26c53e..a318a44 100644
--- a/src/test/java/au/com/acegi/xmlformat/XmlCheckPluginTest.java
+++ b/src/test/java/au/com/acegi/xmlformat/XmlCheckPluginTest.java
@@ -152,6 +152,25 @@ void pluginSucceedsWhenAllFormatted() throws MojoExecutionException, MojoFailure
verify(log, atLeastOnce()).debug(anyString());
}
+ @Test
+ void pluginSkip() throws MojoExecutionException, MojoFailureException {
+ final XmlCheckPlugin plugin = new XmlCheckPlugin();
+ plugin.setLog(log);
+ plugin.setSkip(true);
+ when(log.isDebugEnabled()).thenReturn(true);
+ when(log.isInfoEnabled()).thenReturn(true);
+ when(log.isErrorEnabled()).thenReturn(true);
+
+ plugin.setBaseDirectory(proj);
+ plugin.setIncludes("**/*.xml");
+ plugin.setTargetDirectory(target);
+
+ // Should not throw even though unformatted files exist
+ plugin.execute();
+
+ verify(log, atLeastOnce()).info("[xml-format] Skipped");
+ }
+
private static File newFolder(final File root, final String... subDirs) throws IOException {
final String subFolder = String.join("/", subDirs);
final File result = root.toPath().resolve(subFolder).toFile();
diff --git a/src/test/java/au/com/acegi/xmlformat/XmlOutputFormatTest.java b/src/test/java/au/com/acegi/xmlformat/XmlOutputFormatTest.java
new file mode 100644
index 0000000..7c5746f
--- /dev/null
+++ b/src/test/java/au/com/acegi/xmlformat/XmlOutputFormatTest.java
@@ -0,0 +1,75 @@
+/*
+ * XML Format Maven Plugin (https://github.com/acegi/xml-format-maven-plugin)
+ *
+ * Copyright 2011-2026 Acegi Technology Pty Limited.
+ *
+ * Licensed 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 au.com.acegi.xmlformat;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link XmlOutputFormat}.
+ */
+public class XmlOutputFormatTest {
+
+ @Test
+ void defaultIndentSizeIsTwo() {
+ final XmlOutputFormat fmt = new XmlOutputFormat();
+ // Default indent is 2 spaces, represented as a string of length 2
+ assertThat(fmt.getIndent().length(), is(2));
+ }
+
+ @Test
+ void defaultNewlinesIsTrue() {
+ final XmlOutputFormat fmt = new XmlOutputFormat();
+ assertThat(fmt.isNewlines(), is(true));
+ }
+
+ @Test
+ void defaultTrimTextIsTrue() {
+ final XmlOutputFormat fmt = new XmlOutputFormat();
+ assertThat(fmt.isTrimText(), is(true));
+ }
+
+ @Test
+ void defaultPadTextIsTrue() {
+ final XmlOutputFormat fmt = new XmlOutputFormat();
+ assertThat(fmt.isPadText(), is(true));
+ }
+
+ @Test
+ void defaultKeepBlankLinesIsFalse() {
+ final XmlOutputFormat fmt = new XmlOutputFormat();
+ assertThat(fmt.isKeepBlankLines(), is(false));
+ }
+
+ @Test
+ void setKeepBlankLinesToTrue() {
+ final XmlOutputFormat fmt = new XmlOutputFormat();
+ fmt.setKeepBlankLines(true);
+ assertThat(fmt.isKeepBlankLines(), is(true));
+ }
+
+ @Test
+ void setKeepBlankLinesToFalse() {
+ final XmlOutputFormat fmt = new XmlOutputFormat();
+ fmt.setKeepBlankLines(true);
+ fmt.setKeepBlankLines(false);
+ assertThat(fmt.isKeepBlankLines(), is(false));
+ }
+}
diff --git a/src/test/resources/it/pom.xml b/src/test/resources/it/pom.xml
new file mode 100644
index 0000000..aeec1c3
--- /dev/null
+++ b/src/test/resources/it/pom.xml
@@ -0,0 +1,26 @@
+
+
+
+ 4.0.0
+ au.com.acegi.xmlformat.test
+ test-project
+ 1.0.0
+