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