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