View Javadoc
1   /*
2    * SPDX-License-Identifier: Apache-2.0
3    * See LICENSE file for details.
4    *
5    * Copyright 2025-2026 Hazendaz
6    * Copyright 2011-2025 Acegi Technology Pty Limited.
7    */
8   package au.com.acegi.xmlformat;
9   
10  import static au.com.acegi.xmlformat.TestUtil.fileToString;
11  import static au.com.acegi.xmlformat.TestUtil.stringToFile;
12  import static org.hamcrest.CoreMatchers.containsString;
13  import static org.hamcrest.CoreMatchers.is;
14  import static org.hamcrest.CoreMatchers.notNullValue;
15  import static org.hamcrest.MatcherAssert.assertThat;
16  import static org.junit.jupiter.api.Assertions.assertThrows;
17  
18  import java.io.File;
19  import java.io.IOException;
20  
21  import org.apache.maven.plugin.MojoExecutionException;
22  import org.apache.maven.plugin.testing.MojoRule;
23  import org.junit.jupiter.api.AfterEach;
24  import org.junit.jupiter.api.BeforeEach;
25  import org.junit.jupiter.api.Test;
26  import org.junit.jupiter.api.io.TempDir;
27  
28  /**
29   * Integration-style tests for {@link AbstractXmlPlugin} subclasses using the Maven Plugin Testing Harness
30   * ({@link MojoRule}).
31   *
32   * <p>
33   * The harness is used specifically for its {@code setVariableValueToObject} utility, which reflects into private
34   * {@code @Parameter} fields that have no package-private setters — such as {@code tabIndent}, {@code lineEnding},
35   * {@code keepBlankLines}, {@code expandEmptyElements}, and {@code indentSize}.
36   *
37   * <p>
38   * Mojos are created directly via {@code new} rather than Plexus container lookup (which requires a pre-generated Sisu
39   * component index). The package-private setters in {@link AbstractXmlPlugin} handle the mandatory parameters while the
40   * harness handles the remainder via reflection.
41   */
42  public class AbstractXmlPluginMojoRuleTest {
43  
44      /** Exposes the protected {@code before}/{@code after} lifecycle hooks for use in JUnit 5. */
45      private static final class TestMojoRule extends MojoRule {
46          @Override
47          public void before() throws Throwable {
48              super.before();
49          }
50  
51          @Override
52          public void after() {
53              super.after();
54          }
55      }
56  
57      private static final String UNFORMATTED_XML = "<root>  <child/>  </root>";
58      private static final String INCLUDE_ALL_XML = "**/*.xml";
59  
60      @TempDir
61      private File tmp;
62  
63      private final TestMojoRule rule = new TestMojoRule();
64  
65      @BeforeEach
66      void setupHarness() throws Throwable {
67          rule.before();
68      }
69  
70      @AfterEach
71      void tearDownHarness() {
72          rule.after();
73      }
74  
75      // -------------------------------------------------------------------------
76      // Mojo instantiation
77      // -------------------------------------------------------------------------
78  
79      @Test
80      void xmlFormatMojoCanBeInstantiated() {
81          assertThat(new XmlFormatPlugin(), notNullValue());
82      }
83  
84      @Test
85      void xmlCheckMojoCanBeInstantiated() {
86          assertThat(new XmlCheckPlugin(), notNullValue());
87      }
88  
89      // -------------------------------------------------------------------------
90      // xml-format goal — parameter variations via MojoRule reflection
91      // -------------------------------------------------------------------------
92  
93      @Test
94      void xmlFormatWithDefaultSettings() throws Exception {
95          final File proj = createProjectDir("default");
96          final File target = createTargetDir(proj);
97          writeXmlFile(proj, "sample.xml", UNFORMATTED_XML);
98  
99          final XmlFormatPlugin mojo = configureMojo(new XmlFormatPlugin(), proj, target);
100         mojo.execute();
101 
102         final String formatted = fileToString(proj.toPath().resolve("sample.xml").toFile());
103         assertThat(formatted, containsString("\n"));
104     }
105 
106     @Test
107     void xmlFormatWithTabIndent() throws Exception {
108         final File proj = createProjectDir("tabindent");
109         final File target = createTargetDir(proj);
110         writeXmlFile(proj, "sample.xml", UNFORMATTED_XML);
111 
112         final XmlFormatPlugin mojo = configureMojo(new XmlFormatPlugin(), proj, target);
113         rule.setVariableValueToObject(mojo, "tabIndent", true);
114         mojo.execute();
115 
116         final String formatted = fileToString(proj.toPath().resolve("sample.xml").toFile());
117         assertThat(formatted, containsString("\t"));
118     }
119 
120     @Test
121     void xmlFormatWithCrlfLineEnding() throws Exception {
122         final File proj = createProjectDir("crlf");
123         final File target = createTargetDir(proj);
124         writeXmlFile(proj, "sample.xml", UNFORMATTED_XML);
125 
126         final XmlFormatPlugin mojo = configureMojo(new XmlFormatPlugin(), proj, target);
127         rule.setVariableValueToObject(mojo, "lineEnding", LineEnding.CRLF);
128         mojo.execute();
129 
130         final String formatted = fileToString(proj.toPath().resolve("sample.xml").toFile());
131         assertThat(formatted, containsString("\r\n"));
132     }
133 
134     @Test
135     void xmlFormatWithCrLineEnding() throws Exception {
136         final File proj = createProjectDir("cr");
137         final File target = createTargetDir(proj);
138         writeXmlFile(proj, "sample.xml", UNFORMATTED_XML);
139 
140         final XmlFormatPlugin mojo = configureMojo(new XmlFormatPlugin(), proj, target);
141         rule.setVariableValueToObject(mojo, "lineEnding", LineEnding.CR);
142         mojo.execute();
143 
144         final String formatted = fileToString(proj.toPath().resolve("sample.xml").toFile());
145         assertThat(formatted, containsString("\r"));
146     }
147 
148     @Test
149     void xmlFormatWithKeepBlankLines() throws Exception {
150         final File proj = createProjectDir("keepblanklines");
151         final File target = createTargetDir(proj);
152         final String xmlWithBlankLine = "<root>\n\n  <child/>\n\n</root>";
153         writeXmlFile(proj, "sample.xml", xmlWithBlankLine);
154 
155         final XmlFormatPlugin mojo = configureMojo(new XmlFormatPlugin(), proj, target);
156         rule.setVariableValueToObject(mojo, "keepBlankLines", true);
157         mojo.execute();
158 
159         final String formatted = fileToString(proj.toPath().resolve("sample.xml").toFile());
160         assertThat(formatted, containsString("\n\n"));
161     }
162 
163     @Test
164     void xmlFormatWithExpandEmptyElements() throws Exception {
165         final File proj = createProjectDir("expand");
166         final File target = createTargetDir(proj);
167         writeXmlFile(proj, "sample.xml", "<root><child/></root>");
168 
169         final XmlFormatPlugin mojo = configureMojo(new XmlFormatPlugin(), proj, target);
170         rule.setVariableValueToObject(mojo, "expandEmptyElements", true);
171         mojo.execute();
172 
173         final String formatted = fileToString(proj.toPath().resolve("sample.xml").toFile());
174         assertThat(formatted, containsString("<child></child>"));
175     }
176 
177     @Test
178     void xmlFormatWithCustomIndentSize() throws Exception {
179         final File proj = createProjectDir("indent4");
180         final File target = createTargetDir(proj);
181         writeXmlFile(proj, "sample.xml", UNFORMATTED_XML);
182 
183         final XmlFormatPlugin mojo = configureMojo(new XmlFormatPlugin(), proj, target);
184         rule.setVariableValueToObject(mojo, "indentSize", 4);
185         mojo.execute();
186 
187         final String formatted = fileToString(proj.toPath().resolve("sample.xml").toFile());
188         assertThat(formatted, containsString("    <child"));
189     }
190 
191     @Test
192     void xmlFormatSkipDoesNotModifyFiles() throws Exception {
193         final File proj = createProjectDir("skip-format");
194         final File target = createTargetDir(proj);
195         writeXmlFile(proj, "sample.xml", UNFORMATTED_XML);
196 
197         final XmlFormatPlugin mojo = configureMojo(new XmlFormatPlugin(), proj, target);
198         mojo.setSkip(true);
199         mojo.execute();
200 
201         assertThat(fileToString(proj.toPath().resolve("sample.xml").toFile()), is(UNFORMATTED_XML));
202     }
203 
204     @Test
205     void xmlFormatEmptyFileIsIgnored() throws Exception {
206         final File proj = createProjectDir("empty");
207         final File target = createTargetDir(proj);
208         writeXmlFile(proj, "empty.xml", "");
209 
210         final XmlFormatPlugin mojo = configureMojo(new XmlFormatPlugin(), proj, target);
211         mojo.execute();
212 
213         assertThat(fileToString(proj.toPath().resolve("empty.xml").toFile()), is(""));
214     }
215 
216     @Test
217     void xmlFormatWithSuppressedDeclaration() throws Exception {
218         final File proj = createProjectDir("suppress");
219         final File target = createTargetDir(proj);
220         writeXmlFile(proj, "sample.xml", UNFORMATTED_XML);
221 
222         final XmlFormatPlugin mojo = configureMojo(new XmlFormatPlugin(), proj, target);
223         rule.setVariableValueToObject(mojo, "suppressDeclaration", true);
224         mojo.execute();
225 
226         final String formatted = fileToString(proj.toPath().resolve("sample.xml").toFile());
227         assertThat(formatted.contains("<?xml"), is(false));
228     }
229 
230     @Test
231     void xmlFormatWithOmitEncoding() throws Exception {
232         final File proj = createProjectDir("omit-encoding");
233         final File target = createTargetDir(proj);
234         writeXmlFile(proj, "sample.xml", UNFORMATTED_XML);
235 
236         final XmlFormatPlugin mojo = configureMojo(new XmlFormatPlugin(), proj, target);
237         rule.setVariableValueToObject(mojo, "omitEncoding", true);
238         mojo.execute();
239 
240         final String formatted = fileToString(proj.toPath().resolve("sample.xml").toFile());
241         // Encoding attribute should be absent from the XML declaration
242         assertThat(formatted.contains("encoding="), is(false));
243     }
244 
245     // -------------------------------------------------------------------------
246     // xml-check goal variations
247     // -------------------------------------------------------------------------
248 
249     @Test
250     void xmlCheckSkipDoesNotFailOnUnformattedFiles() throws Exception {
251         final File proj = createProjectDir("skip-check");
252         final File target = createTargetDir(proj);
253         writeXmlFile(proj, "sample.xml", UNFORMATTED_XML);
254 
255         final XmlCheckPlugin mojo = configureMojo(new XmlCheckPlugin(), proj, target);
256         mojo.setSkip(true);
257         mojo.execute();
258     }
259 
260     @Test
261     void xmlCheckPassesOnAlreadyFormattedFile() throws Exception {
262         final File proj = createProjectDir("check-pass");
263         final File target = createTargetDir(proj);
264         writeXmlFile(proj, "sample.xml", UNFORMATTED_XML);
265 
266         // First format the file so that the check will pass
267         configureMojo(new XmlFormatPlugin(), proj, target).execute();
268 
269         // Now verify the check mojo considers the file already formatted
270         configureMojo(new XmlCheckPlugin(), proj, target).execute();
271     }
272 
273     @Test
274     void xmlCheckFailsOnUnformattedFile() throws Exception {
275         final File proj = createProjectDir("check-fail");
276         final File target = createTargetDir(proj);
277         writeXmlFile(proj, "sample.xml", UNFORMATTED_XML);
278 
279         final XmlCheckPlugin mojo = configureMojo(new XmlCheckPlugin(), proj, target);
280         assertThrows(MojoExecutionException.class, mojo::execute);
281     }
282 
283     // -------------------------------------------------------------------------
284     // Helpers
285     // -------------------------------------------------------------------------
286 
287     private File createProjectDir(final String name) throws IOException {
288         final File proj = tmp.toPath().resolve(name).toFile();
289         if (!proj.mkdirs()) {
290             throw new IOException("Could not create project dir: " + proj);
291         }
292         return proj;
293     }
294 
295     private File createTargetDir(final File proj) throws IOException {
296         final File target = proj.toPath().resolve("target").toFile();
297         if (!target.mkdirs()) {
298             throw new IOException("Could not create target dir: " + target);
299         }
300         return target;
301     }
302 
303     private void writeXmlFile(final File dir, final String filename, final String content) {
304         stringToFile(content, dir.toPath().resolve(filename).toFile());
305     }
306 
307     /**
308      * Applies the mandatory parameters to any {@link AbstractXmlPlugin} mojo via the package-private setters.
309      * Additionally uses the harness reflection helper to set the {@code @Parameter} fields whose Maven default values
310      * differ from the Java field defaults (because no explicit field initializer is present in the source).
311      */
312     private <T extends AbstractXmlPlugin> T configureMojo(final T mojo, final File baseDir, final File targetDir)
313             throws IllegalAccessException {
314         mojo.setBaseDirectory(baseDir);
315         mojo.setTargetDirectory(targetDir);
316         mojo.setIncludes(INCLUDE_ALL_XML);
317         mojo.setExcludes();
318         // These @Parameter fields have defaultValue != Java field default, so we must set them explicitly.
319         rule.setVariableValueToObject(mojo, "newlines", true);
320         rule.setVariableValueToObject(mojo, "trimText", true);
321         rule.setVariableValueToObject(mojo, "indentSize", 2);
322         return mojo;
323     }
324 }