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