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