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.internal;
21  
22  import static com.google.common.truth.Truth.assertWithMessage;
23  
24  import java.io.File;
25  import java.io.IOException;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.util.ArrayList;
29  import java.util.HashMap;
30  import java.util.List;
31  import java.util.Locale;
32  import java.util.Map;
33  import java.util.function.Consumer;
34  import java.util.stream.Stream;
35  
36  import org.junit.jupiter.api.Test;
37  
38  /**
39   * AllTestsTest.
40   *
41   * @noinspection ClassIndependentOfModule
42   * @noinspectionreason ClassIndependentOfModule - architecture of
43   *      test modules requires this structure
44   */
45  public class AllTestsTest {
46  
47      @Test
48      public void testAllInputsHaveTest() throws Exception {
49          final Map<String, List<String>> allTests = new HashMap<>();
50  
51          walkVisible(Path.of("src/test/java"), filePath -> {
52              grabAllTests(allTests, filePath.toFile());
53          });
54  
55          assertWithMessage("found tests")
56              .that(allTests.keySet())
57              .isNotEmpty();
58  
59          walkVisible(Path.of("src/test/resources/com/puppycrawl"), filePath -> {
60              verifyInputFile(allTests, filePath.toFile());
61          });
62          walkVisible(Path.of("src/test/resources-noncompilable/com/puppycrawl"), filePath -> {
63              verifyInputFile(allTests, filePath.toFile());
64          });
65      }
66  
67      @Test
68      public void testAllTestsHaveProductionCode() throws Exception {
69          final Map<String, List<String>> allTests = new HashMap<>();
70  
71          walkVisible(Path.of("src/main/java"), filePath -> {
72              grabAllFiles(allTests, filePath.toFile());
73          });
74  
75          assertWithMessage("found tests")
76              .that(allTests.keySet())
77              .isNotEmpty();
78  
79          walkVisible(Path.of("src/test/java"), filePath -> {
80              verifyHasProductionFile(allTests, filePath.toFile());
81          });
82      }
83  
84      /**
85       * Walks through the file tree rooted at the specified path and performs the given action
86       * on each visible (non-hidden) file path.
87       *
88       * <p>This method recursively traverses the directory tree starting from the specified
89       * {@code path}. It filters out hidden files and directories before applying the provided
90       * {@code action} to each visible file path. The definition of what constitutes a hidden
91       * file or directory is operating system dependent, and this method uses the underlying
92       * file system's criteria for hidden files.</p>
93       *
94       * @param path   the starting path for the file tree traversal
95       * @param action the action to be performed on each visible file path
96       * @throws IOException if an I/O error occurs while accessing the file system
97       */
98      private static void walkVisible(Path path, Consumer<Path> action) throws IOException {
99          try (Stream<Path> walk = Files.walk(path)) {
100             walk.filter(filePath -> !filePath.toFile().isHidden())
101                 .forEach(action);
102         }
103     }
104 
105     private static void grabAllTests(Map<String, List<String>> allTests, File file) {
106         if (file.isFile() && file.getName().endsWith("Test.java")) {
107             String path;
108 
109             try {
110                 path = getSimplePath(file.getCanonicalPath()).replace("CheckTest.java", "")
111                         .replace("Test.java", "");
112             }
113             catch (IOException ex) {
114                 throw new IllegalStateException(ex);
115             }
116 
117             // override for 'AbstractCheck' naming
118             if (path.endsWith(File.separator + "Abstract")) {
119                 path += "Check";
120             }
121 
122             final int slash = path.lastIndexOf(File.separatorChar);
123             final String packge = path.substring(0, slash);
124             final List<String> classes = allTests.computeIfAbsent(packge, key -> new ArrayList<>());
125 
126             classes.add(path.substring(slash + 1));
127         }
128     }
129 
130     private static void grabAllFiles(Map<String, List<String>> allTests, File file) {
131         if (file.isFile()) {
132             final String path;
133 
134             try {
135                 path = getSimplePath(file.getCanonicalPath());
136             }
137             catch (IOException ex) {
138                 throw new IllegalStateException(ex);
139             }
140 
141             final int slash = path.lastIndexOf(File.separatorChar);
142             final String packge = path.substring(0, slash);
143             final List<String> classes = allTests.computeIfAbsent(packge, key -> new ArrayList<>());
144 
145             classes.add(path.substring(slash + 1));
146         }
147     }
148 
149     private static void verifyInputFile(Map<String, List<String>> allTests, File file) {
150         if (file.isFile()) {
151             final String path;
152 
153             try {
154                 path = getSimplePath(file.getCanonicalPath());
155             }
156             catch (IOException ex) {
157                 throw new IllegalStateException(ex);
158             }
159 
160             // until https://github.com/checkstyle/checkstyle/issues/5105
161             if (shouldSkipFileProcessing(path)) {
162                 String fileName = file.getName();
163                 final boolean skipFileNaming = shouldSkipInputFileNameCheck(path, fileName);
164 
165                 if (!skipFileNaming) {
166                     assertWithMessage("Resource must start with 'Input' or 'Expected': " + path)
167                             .that(fileName.startsWith("Input") || fileName.startsWith("Expected"))
168                             .isTrue();
169 
170                     if (fileName.startsWith("Input")) {
171                         fileName = fileName.substring(5);
172                     }
173                     else {
174                         fileName = fileName.substring(8);
175                     }
176 
177                     final int period = fileName.lastIndexOf('.');
178 
179                     if (period > 0) {
180                         fileName = fileName.substring(0, period);
181                     }
182                 }
183 
184                 verifyInputFile(allTests, skipFileNaming, path, fileName);
185             }
186         }
187     }
188 
189     private static void verifyInputFile(Map<String, List<String>> allTests, boolean skipFileNaming,
190             String path, String fileName) {
191         List<String> classes;
192         int slash = path.lastIndexOf(File.separatorChar);
193         String packge = path.substring(0, slash);
194         boolean found = false;
195 
196         for (int depth = 0; depth < 4; depth++) {
197             // -@cs[MoveVariableInsideIf] assignment value is modified later, so it can't be
198             // moved
199             final String folderPath = packge;
200             slash = packge.lastIndexOf(File.separatorChar);
201             packge = path.substring(0, slash);
202             classes = allTests.get(packge);
203 
204             if (classes != null
205                     && checkInputMatchCorrectFileStructure(classes, folderPath, skipFileNaming,
206                             fileName)) {
207                 found = true;
208                 break;
209             }
210         }
211 
212         assertWithMessage("Resource must be named after a Test like 'InputMyCustomCase.java' "
213                 + "and be in the sub-package of the test like 'mycustom' "
214                 + "for test 'MyCustomCheckTest': " + path)
215                 .that(found)
216                 .isTrue();
217     }
218 
219     /**
220      * Checks if the file processing should be skipped based on the path.
221      *
222      * @param path The path to check for skip conditions.
223      * @return true if file processing should be skipped, false otherwise.
224      */
225     private static boolean shouldSkipFileProcessing(String path) {
226         return !path.contains(File.separatorChar + "grammar" + File.separatorChar)
227                 && !path.contains(File.separatorChar + "foo" + File.separatorChar)
228                 && !path.contains(File.separatorChar + "bar" + File.separatorChar)
229                 && !path.contains(File.separator + "abc" + File.separatorChar)
230                 && !path.contains(File.separator + "zoo" + File.separatorChar);
231     }
232 
233     private static void verifyHasProductionFile(Map<String, List<String>> allTests, File file) {
234         if (file.isFile()) {
235             final String fileName = file.getName().replace("Test.java", ".java");
236 
237             if (isTarget(file, fileName)) {
238                 final String path;
239 
240                 try {
241                     path = getSimplePath(file.getCanonicalPath());
242                 }
243                 catch (IOException ex) {
244                     throw new IllegalStateException(ex);
245                 }
246 
247                 if (!path.contains(File.separatorChar + "grammar" + File.separatorChar)
248                         && !path.contains(File.separatorChar + "internal" + File.separatorChar)) {
249                     final int slash = path.lastIndexOf(File.separatorChar);
250                     final String packge = path.substring(0, slash);
251                     final List<String> classes = allTests.get(packge);
252 
253                     assertWithMessage("Test must be named after a production class "
254                                + "and must be in the same package of the production class: " + path)
255                             .that(classes)
256                             .contains(fileName);
257                 }
258             }
259         }
260     }
261 
262     private static boolean isTarget(File file, String fileName) {
263         return !fileName.endsWith("TestSupport.java")
264                 // tests external utility XPathEvaluator
265                 && !"XpathMapper.java".equals(fileName)
266                 // JavadocMetadataScraper and related classes are temporarily hosted in test
267                 && !file.getPath().contains("meta")
268                 // InlineConfigParser is hosted in test
269                 && !file.getPath().contains("bdd")
270                 // Annotation to suppress invocation of forbidden apis
271                 && !"SuppressForbiddenApi.java".equals(fileName);
272     }
273 
274     private static boolean checkInputMatchCorrectFileStructure(List<String> classes,
275             String folderPath, boolean skipFileNaming, String fileName) {
276         boolean result = false;
277 
278         for (String clss : classes) {
279             if (folderPath.endsWith(File.separatorChar + clss.toLowerCase(Locale.ENGLISH))
280                     && (skipFileNaming || fileName.startsWith(clss))) {
281                 result = true;
282                 break;
283             }
284         }
285 
286         return result;
287     }
288 
289     private static boolean shouldSkipInputFileNameCheck(String path, String fileName) {
290         return "package-info.java".equals(fileName)
291                 || "package.html".equals(fileName)
292                 // special directory for files that can't be renamed or are secondary inputs
293                 || path.contains(File.separatorChar + "inputs" + File.separatorChar)
294                 // all inputs must start with 'messages'
295                 || path.contains(File.separatorChar + "translation" + File.separatorChar);
296     }
297 
298     private static String getSimplePath(String path) {
299         return path.substring(path.lastIndexOf("com" + File.separator + "puppycrawl"));
300     }
301 
302 }