View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks;
21  
22  import static com.google.common.truth.Truth.assertWithMessage;
23  import static com.puppycrawl.tools.checkstyle.checks.TranslationCheck.MSG_KEY;
24  import static com.puppycrawl.tools.checkstyle.checks.TranslationCheck.MSG_KEY_MISSING_TRANSLATION_FILE;
25  
26  import java.io.ByteArrayOutputStream;
27  import java.io.File;
28  import java.io.IOException;
29  import java.io.Writer;
30  import java.lang.reflect.Field;
31  import java.nio.charset.StandardCharsets;
32  import java.nio.file.Files;
33  import java.util.Collections;
34  import java.util.Set;
35  import java.util.SortedSet;
36  import java.util.TreeSet;
37  
38  import org.junit.jupiter.api.Test;
39  import org.junit.jupiter.api.io.TempDir;
40  import org.w3c.dom.Node;
41  
42  import com.google.common.collect.ImmutableMap;
43  import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean.OutputStreamOptions;
44  import com.puppycrawl.tools.checkstyle.AbstractXmlTestSupport;
45  import com.puppycrawl.tools.checkstyle.Checker;
46  import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
47  import com.puppycrawl.tools.checkstyle.Definitions;
48  import com.puppycrawl.tools.checkstyle.XMLLogger;
49  import com.puppycrawl.tools.checkstyle.api.Configuration;
50  import com.puppycrawl.tools.checkstyle.api.FileText;
51  import com.puppycrawl.tools.checkstyle.api.MessageDispatcher;
52  import com.puppycrawl.tools.checkstyle.api.Violation;
53  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
54  import com.puppycrawl.tools.checkstyle.internal.utils.XmlUtil;
55  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
56  
57  public class TranslationCheckTest extends AbstractXmlTestSupport {
58  
59      @TempDir
60      public File temporaryFolder;
61  
62      @Override
63      protected String getPackageLocation() {
64          return "com/puppycrawl/tools/checkstyle/checks/translation";
65      }
66  
67      @Test
68      public void testTranslation() throws Exception {
69          final Configuration checkConfig = createModuleConfig(TranslationCheck.class);
70          final String[] expected = {
71              "1: " + getCheckMessage(MSG_KEY, "only.english"),
72          };
73          final File[] propertyFiles = {
74              new File(getPath("messages_test_de.properties")),
75              new File(getPath("messages_test.properties")),
76          };
77          verify(
78              createChecker(checkConfig),
79              propertyFiles,
80              getPath("messages_test_de.properties"),
81              expected);
82      }
83  
84      @Test
85      public void testDifferentBases() throws Exception {
86          final Configuration checkConfig = createModuleConfig(TranslationCheck.class);
87          final String[] expected = {
88              "1: " + getCheckMessage(MSG_KEY, "only.english"),
89          };
90          final File[] propertyFiles = {
91              new File(getPath("messages_test_de.properties")),
92              new File(getPath("messages_test.properties")),
93              new File(getPath("messages_translation.properties")),
94              new File(getPath("messages_translation_de.properties")),
95          };
96          verify(
97              createChecker(checkConfig),
98              propertyFiles,
99              getPath("messages_test_de.properties"),
100             expected);
101     }
102 
103     @Test
104     public void testDifferentPaths() throws Exception {
105         final File file = new File(temporaryFolder, "messages_test_de.properties");
106         try (Writer writer = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) {
107             final String content = "hello=Hello\ncancel=Cancel";
108             writer.write(content);
109         }
110         final Configuration checkConfig = createModuleConfig(TranslationCheck.class);
111         final String[] expected = {
112             "1: " + getCheckMessage(MSG_KEY_MISSING_TRANSLATION_FILE,
113                     "messages_test.properties"),
114         };
115         final File[] propertyFiles = {
116             file,
117             new File(getPath("messages_test.properties")),
118         };
119         verify(
120             createChecker(checkConfig),
121             propertyFiles,
122             file.getParent(),
123             expected);
124     }
125 
126     /**
127      * Even when we pass several files to AbstractModuleTestSupport#verify,
128      * the check processes it during one run, so we cannot reproduce situation
129      * when TranslationCheck#beginProcessing called several times during single run.
130      * So, we have to use reflection to check this particular case.
131      *
132      * @throws Exception when code tested throws exception
133      */
134     @Test
135     public void testStateIsCleared() throws Exception {
136         final File fileToProcess = new File(
137                 getPath("InputTranslationCheckFireErrors_de.properties")
138         );
139         final String charset = StandardCharsets.UTF_8.name();
140         final TranslationCheck check = new TranslationCheck();
141         check.beginProcessing(charset);
142         check.processFiltered(fileToProcess, new FileText(fileToProcess, charset));
143         check.beginProcessing(charset);
144         final Field field = check.getClass().getDeclaredField("filesToProcess");
145         field.setAccessible(true);
146 
147         assertWithMessage("Stateful field is not cleared on beginProcessing")
148                 .that((Iterable<?>) field.get(check))
149                 .isEmpty();
150     }
151 
152     @Test
153     public void testFileExtension() throws Exception {
154         final DefaultConfiguration checkConfig = createModuleConfig(TranslationCheck.class);
155         checkConfig.addProperty("baseName", "^InputTranslation.*$");
156         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
157         final File[] propertyFiles = {
158             new File(getPath("InputTranslation_de.txt")),
159         };
160         verify(createChecker(checkConfig),
161             propertyFiles,
162             getPath("InputTranslation_de.txt"),
163             expected);
164     }
165 
166     @Test
167     public void testLogOutput() throws Exception {
168         final DefaultConfiguration checkConfig = createModuleConfig(TranslationCheck.class);
169         checkConfig.addProperty("requiredTranslations", "ja,de");
170         checkConfig.addProperty("baseName", "^InputTranslation.*$");
171         final Checker checker = createChecker(checkConfig);
172         checker.setBasedir(getPath(""));
173         final ByteArrayOutputStream out = new ByteArrayOutputStream();
174         final XMLLogger logger = new XMLLogger(out, OutputStreamOptions.NONE);
175         checker.addListener(logger);
176 
177         final String defaultProps = getPath("InputTranslationCheckFireErrors.properties");
178         final String translationProps = getPath("InputTranslationCheckFireErrors_de.properties");
179 
180         final File[] propertyFiles = {
181             new File(defaultProps),
182             new File(translationProps),
183         };
184 
185         final String line = "1: ";
186         final String firstErrorMessage = getCheckMessage(MSG_KEY_MISSING_TRANSLATION_FILE,
187                 "InputTranslationCheckFireErrors_ja.properties");
188         final String secondErrorMessage = getCheckMessage(MSG_KEY, "anotherKey");
189 
190         verify(checker, propertyFiles, ImmutableMap.of(
191             ":1", Collections.singletonList(" " + firstErrorMessage),
192             "InputTranslationCheckFireErrors_de.properties",
193                 Collections.singletonList(line + secondErrorMessage)));
194 
195         verifyXml(getPath("ExpectedTranslationLog.xml"), out,
196             TranslationCheckTest::isFilenamesEqual,
197             firstErrorMessage, secondErrorMessage);
198     }
199 
200     @Test
201     public void testOnePropertyFileSet() throws Exception {
202         final DefaultConfiguration checkConfig = createModuleConfig(TranslationCheck.class);
203         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
204         final File[] propertyFiles = {
205             new File(getPath("app-dev.properties")),
206         };
207         verify(
208             createChecker(checkConfig),
209             propertyFiles,
210             getPath("app-dev.properties"),
211             expected);
212     }
213 
214     @Test
215     public void testLogIoExceptionFileNotFound() throws Exception {
216         // I can't put wrong file here. Checkstyle fails before check started.
217         // I saw some usage of file or handling of wrong file in Checker, or somewhere
218         // in checks running part. So I had to do it with reflection to improve coverage.
219         final TranslationCheck check = new TranslationCheck();
220         final DefaultConfiguration checkConfig = createModuleConfig(TranslationCheck.class);
221         final TestMessageDispatcher dispatcher = new TestMessageDispatcher();
222         check.configure(checkConfig);
223         check.setMessageDispatcher(dispatcher);
224 
225         final Set<String> keys = TestUtil.invokeMethod(check, "getTranslationKeys",
226                 new File(".no.such.file"));
227         assertWithMessage("Translation keys should be empty when File is not found")
228                 .that(keys)
229                 .isEmpty();
230 
231         assertWithMessage("expected number of errors to fire")
232             .that(dispatcher.savedErrors)
233             .hasSize(1);
234         final Violation violation = new Violation(0,
235                 Definitions.CHECKSTYLE_BUNDLE, "general.fileNotFound",
236                 null, null, TranslationCheck.class, null);
237         assertWithMessage("Invalid violation")
238             .that(dispatcher.savedErrors.iterator().next())
239             .isEqualTo(violation);
240     }
241 
242     @Test
243     public void testLogIoException() throws Exception {
244         // I can't put wrong file here. Checkstyle fails before check started.
245         // I saw some usage of file or handling of wrong file in Checker, or somewhere
246         // in checks running part. So I had to do it with reflection to improve coverage.
247         final TranslationCheck check = new TranslationCheck();
248         final DefaultConfiguration checkConfig = createModuleConfig(TranslationCheck.class);
249         final TestMessageDispatcher dispatcher = new TestMessageDispatcher();
250         check.configure(checkConfig);
251         check.setMessageDispatcher(dispatcher);
252         check.setId("ID1");
253 
254         final Exception exception = new IOException("test exception");
255         TestUtil.invokeMethod(check, "logException", exception, new File(""));
256 
257         assertWithMessage("expected number of errors to fire")
258             .that(dispatcher.savedErrors.size())
259             .isEqualTo(1);
260         final Violation violation = new Violation(0,
261                 Definitions.CHECKSTYLE_BUNDLE, "general.exception",
262                 new String[] {exception.getMessage()}, "ID1", TranslationCheck.class, null);
263         assertWithMessage("Invalid violation")
264             .that(dispatcher.savedErrors.iterator().next())
265             .isEqualTo(violation);
266     }
267 
268     @Test
269     public void testLogIllegalArgumentException() throws Exception {
270         final DefaultConfiguration checkConfig = createModuleConfig(TranslationCheck.class);
271         checkConfig.addProperty("baseName", "^bad.*$");
272         final String[] expected = {
273             "0: " + getCheckMessage(Checker.class, "general.exception",
274                     "Malformed \\uxxxx encoding."),
275             "1: " + getCheckMessage(MSG_KEY, "test"),
276         };
277         final File[] propertyFiles = {
278             new File(getPath("bad.properties")),
279             new File(getPath("bad_es.properties")),
280         };
281         verify(
282             createChecker(checkConfig),
283             propertyFiles,
284             getPath("bad.properties"),
285             expected);
286     }
287 
288     @Test
289     public void testDefaultTranslationFileIsMissing() throws Exception {
290         final DefaultConfiguration checkConfig = createModuleConfig(TranslationCheck.class);
291         checkConfig.addProperty("requiredTranslations", "ja,,, de, ja");
292 
293         final File[] propertyFiles = {
294             new File(getPath("messages_translation_de.properties")),
295             new File(getPath("messages_translation_ja.properties")),
296         };
297 
298         final String[] expected = {
299             "1: " + getCheckMessage(MSG_KEY_MISSING_TRANSLATION_FILE,
300                     "messages_translation.properties"),
301         };
302         verify(
303             createChecker(checkConfig),
304             propertyFiles,
305             getPath(""),
306             expected);
307     }
308 
309     @Test
310     public void testTranslationFilesAreMissing() throws Exception {
311         final DefaultConfiguration checkConfig = createModuleConfig(TranslationCheck.class);
312         checkConfig.addProperty("requiredTranslations", "ja, de");
313 
314         final File[] propertyFiles = {
315             new File(getPath("messages_translation.properties")),
316             new File(getPath("messages_translation_ja.properties")),
317         };
318 
319         final String[] expected = {
320             "1: " + getCheckMessage(MSG_KEY_MISSING_TRANSLATION_FILE,
321                     "messages_translation_de.properties"),
322         };
323         verify(
324             createChecker(checkConfig),
325             propertyFiles,
326             getPath(""),
327             expected);
328     }
329 
330     @Test
331     public void testBaseNameWithSeparatorDefaultTranslationIsMissing() throws Exception {
332         final DefaultConfiguration checkConfig = createModuleConfig(TranslationCheck.class);
333         checkConfig.addProperty("requiredTranslations", "fr");
334 
335         final File[] propertyFiles = {
336             new File(getPath("messages-translation_fr.properties")),
337         };
338 
339         final String[] expected = {
340             "1: " + getCheckMessage(MSG_KEY_MISSING_TRANSLATION_FILE,
341                     "messages-translation.properties"),
342         };
343         verify(
344             createChecker(checkConfig),
345             propertyFiles,
346             getPath(""),
347             expected);
348     }
349 
350     @Test
351     public void testBaseNameWithSeparatorTranslationsAreMissing() throws Exception {
352         final DefaultConfiguration checkConfig = createModuleConfig(TranslationCheck.class);
353         checkConfig.addProperty("requiredTranslations", "fr, tr");
354 
355         final File[] propertyFiles = {
356             new File(getPath("messages-translation.properties")),
357             new File(getPath("messages-translation_fr.properties")),
358         };
359 
360         final String[] expected = {
361             "1: " + getCheckMessage(MSG_KEY_MISSING_TRANSLATION_FILE,
362                     "messages-translation_tr.properties"),
363         };
364         verify(
365             createChecker(checkConfig),
366             propertyFiles,
367             getPath(""),
368             expected);
369     }
370 
371     @Test
372     public void testIsNotMessagesBundle() throws Exception {
373         final DefaultConfiguration checkConfig = createModuleConfig(TranslationCheck.class);
374         checkConfig.addProperty("requiredTranslations", "de");
375 
376         final File[] propertyFiles = {
377             new File(getPath("app-dev.properties")),
378             new File(getPath("app-stage.properties")),
379         };
380 
381         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
382         verify(
383             createChecker(checkConfig),
384             propertyFiles,
385             getPath("app-dev.properties"),
386             expected);
387     }
388 
389     @Test
390     public void testTranslationFileWithLanguageCountryVariantIsMissing() throws Exception {
391         final DefaultConfiguration checkConfig = createModuleConfig(TranslationCheck.class);
392         checkConfig.addProperty("requiredTranslations", "es, de");
393 
394         final File[] propertyFiles = {
395             new File(getPath("messages_home.properties")),
396             new File(getPath("messages_home_es_US.properties")),
397             new File(getPath("messages_home_fr_CA_UNIX.properties")),
398             };
399 
400         final String[] expected = {
401             "1: " + getCheckMessage(MSG_KEY_MISSING_TRANSLATION_FILE,
402                     "messages_home_de.properties"),
403         };
404         verify(
405             createChecker(checkConfig),
406             propertyFiles,
407             getPath(""),
408             expected);
409     }
410 
411     @Test
412     public void testTranslationFileWithLanguageCountryVariantArePresent() throws Exception {
413         final DefaultConfiguration checkConfig = createModuleConfig(TranslationCheck.class);
414         checkConfig.addProperty("requiredTranslations", "es, fr");
415 
416         final File[] propertyFiles = {
417             new File(getPath("messages_home.properties")),
418             new File(getPath("messages_home_es_US.properties")),
419             new File(getPath("messages_home_fr_CA_UNIX.properties")),
420             };
421 
422         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
423         verify(
424             createChecker(checkConfig),
425             propertyFiles,
426             getPath(""),
427             expected);
428     }
429 
430     @Test
431     public void testBaseNameOption() throws Exception {
432         final DefaultConfiguration checkConfig = createModuleConfig(TranslationCheck.class);
433         checkConfig.addProperty("requiredTranslations", "de, es, fr, ja");
434         checkConfig.addProperty("baseName", "^.*Labels$");
435 
436         final File[] propertyFiles = {
437             new File(getPath("ButtonLabels.properties")),
438             new File(getPath("ButtonLabels_de.properties")),
439             new File(getPath("ButtonLabels_es.properties")),
440             new File(getPath("ButtonLabels_fr_CA_UNIX.properties")),
441             new File(getPath("messages_home.properties")),
442             new File(getPath("messages_home_es_US.properties")),
443             new File(getPath("messages_home_fr_CA_UNIX.properties")),
444         };
445 
446         final String[] expected = {
447             "1: " + getCheckMessage(MSG_KEY_MISSING_TRANSLATION_FILE,
448                     "ButtonLabels_ja.properties"),
449         };
450         verify(
451             createChecker(checkConfig),
452             propertyFiles,
453             getPath(""),
454             expected);
455     }
456 
457     @Test
458     public void testFileExtensions() throws Exception {
459         final DefaultConfiguration checkConfig = createModuleConfig(TranslationCheck.class);
460         checkConfig.addProperty("requiredTranslations", "de, es, fr, ja");
461         checkConfig.addProperty("fileExtensions", "properties,translation");
462         checkConfig.addProperty("baseName", "^.*(Titles|Labels)$");
463 
464         final File[] propertyFiles = {
465             new File(getPath("ButtonLabels.properties")),
466             new File(getPath("ButtonLabels_de.properties")),
467             new File(getPath("ButtonLabels_es.properties")),
468             new File(getPath("ButtonLabels_fr_CA_UNIX.properties")),
469             new File(getPath("PageTitles.translation")),
470             new File(getPath("PageTitles_de.translation")),
471             new File(getPath("PageTitles_es.translation")),
472             new File(getPath("PageTitles_fr.translation")),
473             new File(getPath("PageTitles_ja.translation")),
474         };
475 
476         final String[] expected = {
477             "1: " + getCheckMessage(MSG_KEY_MISSING_TRANSLATION_FILE,
478                     "ButtonLabels_ja.properties"),
479         };
480 
481         verify(
482             createChecker(checkConfig),
483             propertyFiles,
484             getPath(""),
485             expected);
486     }
487 
488     @Test
489     public void testEqualBaseNamesButDifferentExtensions() throws Exception {
490         final DefaultConfiguration checkConfig = createModuleConfig(TranslationCheck.class);
491         checkConfig.addProperty("requiredTranslations", "de, es, fr, ja");
492         checkConfig.addProperty("fileExtensions", "properties,translations");
493         checkConfig.addProperty("baseName", "^.*Labels$");
494 
495         final File[] propertyFiles = {
496             new File(getPath("ButtonLabels.properties")),
497             new File(getPath("ButtonLabels_de.properties")),
498             new File(getPath("ButtonLabels_es.properties")),
499             new File(getPath("ButtonLabels_fr_CA_UNIX.properties")),
500             new File(getPath("ButtonLabels.translations")),
501             new File(getPath("ButtonLabels_ja.translations")),
502             new File(getPath("ButtonLabels_es.translations")),
503             new File(getPath("ButtonLabels_fr_CA_UNIX.translations")),
504             new File(getPath("ButtonLabels_de.translations")),
505         };
506 
507         final String[] expected = {
508             "1: " + getCheckMessage(MSG_KEY_MISSING_TRANSLATION_FILE,
509                     "ButtonLabels_ja.properties"),
510         };
511 
512         verify(
513             createChecker(checkConfig),
514             propertyFiles,
515             getPath(""),
516             expected);
517     }
518 
519     @Test
520     public void testEqualBaseNamesButDifferentExtensions2() throws Exception {
521         final DefaultConfiguration checkConfig = createModuleConfig(TranslationCheck.class);
522         checkConfig.addProperty("requiredTranslations", "de, es");
523         checkConfig.addProperty("fileExtensions", "properties, translations");
524         checkConfig.addProperty("baseName", "^.*Labels$");
525 
526         final File[] propertyFiles = {
527             new File(getPath("ButtonLabels.properties")),
528             new File(getPath("ButtonLabels_de.properties")),
529             new File(getPath("ButtonLabels_es.properties")),
530             new File(getPath("ButtonLabels_ja.translations")),
531         };
532 
533         final String[] expected = {
534             "1: " + getCheckMessage(MSG_KEY_MISSING_TRANSLATION_FILE,
535                     "ButtonLabels.translations"),
536             "1: " + getCheckMessage(MSG_KEY_MISSING_TRANSLATION_FILE,
537                     "ButtonLabels_de.translations"),
538             "1: " + getCheckMessage(MSG_KEY_MISSING_TRANSLATION_FILE,
539                     "ButtonLabels_es.translations"),
540         };
541 
542         verify(
543             createChecker(checkConfig),
544             propertyFiles,
545             getPath(""),
546             expected);
547     }
548 
549     @Test
550     public void testRegexpToMatchPartOfBaseName() throws Exception {
551         final DefaultConfiguration checkConfig = createModuleConfig(TranslationCheck.class);
552         checkConfig.addProperty("requiredTranslations", "de, es, fr, ja");
553         checkConfig.addProperty("fileExtensions", "properties,translations");
554         checkConfig.addProperty("baseName", "^.*Labels.*");
555 
556         final File[] propertyFiles = {
557             new File(getPath("MyLabelsI18.properties")),
558             new File(getPath("MyLabelsI18_de.properties")),
559             new File(getPath("MyLabelsI18_es.properties")),
560         };
561 
562         final String[] expected = {
563             "1: " + getCheckMessage(MSG_KEY_MISSING_TRANSLATION_FILE, "MyLabelsI18_fr.properties"),
564             "1: " + getCheckMessage(MSG_KEY_MISSING_TRANSLATION_FILE, "MyLabelsI18_ja.properties"),
565         };
566 
567         verify(
568             createChecker(checkConfig),
569             propertyFiles,
570             getPath(""),
571             expected);
572     }
573 
574     @Test
575     public void testBundlesWithSameNameButDifferentPaths() throws Exception {
576         final DefaultConfiguration checkConfig = createModuleConfig(TranslationCheck.class);
577         checkConfig.addProperty("requiredTranslations", "de");
578         checkConfig.addProperty("fileExtensions", "properties");
579         checkConfig.addProperty("baseName", "^.*Labels.*");
580 
581         final File[] propertyFiles = {
582             new File(getPath("MyLabelsI18.properties")),
583             new File(getPath("MyLabelsI18_de.properties")),
584             new File(getNonCompilablePath("MyLabelsI18.properties")),
585             new File(getNonCompilablePath("MyLabelsI18_de.properties")),
586         };
587 
588         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
589 
590         verify(
591             createChecker(checkConfig),
592             propertyFiles,
593             getPath(""),
594             expected);
595     }
596 
597     @Test
598     public void testWrongUserSpecifiedLanguageCodes() {
599         final TranslationCheck check = new TranslationCheck();
600         try {
601             check.setRequiredTranslations("11");
602             assertWithMessage(
603                     "IllegalArgumentException is expected. Specified language code is incorrect.")
604                             .fail();
605         }
606         catch (IllegalArgumentException ex) {
607             final String exceptionMessage = ex.getMessage();
608             assertWithMessage("Error message is unexpected")
609                     .that(exceptionMessage)
610                     .contains("11");
611         }
612     }
613 
614     /**
615      * Compare two file names.
616      *
617      * @param expected expected node
618      * @param actual actual node
619      * @return true if file names match
620      */
621     private static boolean isFilenamesEqual(Node expected, Node actual) {
622         // order is not always maintained here for an unknown reason.
623         // File names can appear in different orders depending on the OS and VM.
624         // This ensures we pick up the correct file based on its name and the
625         // number of children it has.
626         return !"file".equals(expected.getNodeName())
627             || XmlUtil.getNameAttributeOfNode(expected)
628             .equals(XmlUtil.getNameAttributeOfNode(actual))
629             && XmlUtil.getChildrenElements(expected).size() == XmlUtil
630             .getChildrenElements(actual).size();
631     }
632 
633     private static final class TestMessageDispatcher implements MessageDispatcher {
634 
635         private Set<Violation> savedErrors;
636 
637         @Override
638         public void fireFileStarted(String fileName) {
639             throw new IllegalStateException(fileName);
640         }
641 
642         @Override
643         public void fireFileFinished(String fileName) {
644             throw new IllegalStateException(fileName);
645         }
646 
647         @Override
648         public void fireErrors(String fileName, SortedSet<Violation> errors) {
649             savedErrors = new TreeSet<>(errors);
650         }
651 
652     }
653 
654 }