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;
21  
22  import static com.google.common.truth.Truth.assertWithMessage;
23  import static com.puppycrawl.tools.checkstyle.internal.utils.TestUtil.getExpectedThrowable;
24  import static org.mockito.ArgumentMatchers.any;
25  import static org.mockito.Mockito.mockStatic;
26  
27  import java.io.BufferedInputStream;
28  import java.io.BufferedReader;
29  import java.io.ByteArrayOutputStream;
30  import java.io.File;
31  import java.io.IOException;
32  import java.io.ObjectOutputStream;
33  import java.net.URI;
34  import java.nio.file.Files;
35  import java.nio.file.Path;
36  import java.security.MessageDigest;
37  import java.security.NoSuchAlgorithmException;
38  import java.util.Comparator;
39  import java.util.HashSet;
40  import java.util.Iterator;
41  import java.util.Locale;
42  import java.util.Properties;
43  import java.util.Set;
44  import java.util.UUID;
45  import java.util.stream.Stream;
46  
47  import org.junit.jupiter.api.Test;
48  import org.junit.jupiter.api.condition.DisabledOnOs;
49  import org.junit.jupiter.api.condition.OS;
50  import org.junit.jupiter.api.io.TempDir;
51  import org.junit.jupiter.params.ParameterizedTest;
52  import org.junit.jupiter.params.provider.ValueSource;
53  import org.mockito.MockedStatic;
54  
55  import com.google.common.io.BaseEncoding;
56  import com.google.common.io.ByteStreams;
57  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
58  import com.puppycrawl.tools.checkstyle.api.Configuration;
59  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
60  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
61  
62  public class PropertyCacheFileTest extends AbstractPathTestSupport {
63  
64      @TempDir
65      public File temporaryFolder;
66  
67      @Override
68      protected String getPackageLocation() {
69          return "com/puppycrawl/tools/checkstyle/propertycachefile";
70      }
71  
72      @Test
73      public void testCtor() {
74          try {
75              final Object test = new PropertyCacheFile(null, "");
76              assertWithMessage("exception expected but got " + test).fail();
77          }
78          catch (IllegalArgumentException exc) {
79              assertWithMessage("Invalid exception message")
80                  .that(exc.getMessage())
81                  .isEqualTo("config can not be null");
82          }
83          final Configuration config = new DefaultConfiguration("myName");
84          try {
85              final Object test = new PropertyCacheFile(config, null);
86              assertWithMessage("exception expected but got " + test).fail();
87          }
88          catch (IllegalArgumentException exc) {
89              assertWithMessage("Invalid exception message")
90                  .that(exc.getMessage())
91                  .isEqualTo("fileName can not be null");
92          }
93      }
94  
95      @Test
96      public void testInCache() {
97          final Configuration config = new DefaultConfiguration("myName");
98          final String uniqueFileName = "junit_" + UUID.randomUUID() + ".java";
99          final File filePath = new File(temporaryFolder, uniqueFileName);
100         final PropertyCacheFile cache = new PropertyCacheFile(config, filePath.toString());
101         cache.put("myFile", 1);
102         assertWithMessage("Should return true when file is in cache")
103                 .that(cache.isInCache("myFile", 1))
104                 .isTrue();
105         assertWithMessage("Should return false when file is not in cache")
106                 .that(cache.isInCache("myFile", 2))
107                 .isFalse();
108         assertWithMessage("Should return false when file is not in cache")
109                 .that(cache.isInCache("myFile1", 1))
110                 .isFalse();
111     }
112 
113     @Test
114     public void testResetIfFileDoesNotExist() throws IOException {
115         final Configuration config = new DefaultConfiguration("myName");
116         final PropertyCacheFile cache = new PropertyCacheFile(config, "fileDoesNotExist.txt");
117 
118         cache.load();
119 
120         assertWithMessage("Config hash key should not be null")
121             .that(cache.get(PropertyCacheFile.CONFIG_HASH_KEY))
122             .isNotNull();
123     }
124 
125     @Test
126     public void testPopulateDetails() throws IOException {
127         final Configuration config = new DefaultConfiguration("myName");
128         final PropertyCacheFile cache = new PropertyCacheFile(config,
129                 getPath("InputPropertyCacheFile"));
130         cache.load();
131 
132         final String hash = cache.get(PropertyCacheFile.CONFIG_HASH_KEY);
133         assertWithMessage("Config hash key should not be null")
134             .that(hash)
135             .isNotNull();
136         assertWithMessage("Should return null if key is not in cache")
137             .that(cache.get("key"))
138             .isNull();
139 
140         cache.load();
141 
142         assertWithMessage("Invalid config hash key")
143             .that(cache.get(PropertyCacheFile.CONFIG_HASH_KEY))
144             .isEqualTo(hash);
145         assertWithMessage("Invalid cache value")
146             .that(cache.get("key"))
147             .isEqualTo("value");
148         assertWithMessage("Config hash key should not be null")
149             .that(cache.get(PropertyCacheFile.CONFIG_HASH_KEY))
150             .isNotNull();
151     }
152 
153     @Test
154     public void testConfigHashOnReset() throws IOException {
155         final Configuration config = new DefaultConfiguration("myName");
156         final String uniqueFileName = "junit_" + UUID.randomUUID() + ".java";
157         final File filePath = new File(temporaryFolder, uniqueFileName);
158         final PropertyCacheFile cache = new PropertyCacheFile(config, filePath.toString());
159 
160         cache.load();
161 
162         final String hash = cache.get(PropertyCacheFile.CONFIG_HASH_KEY);
163         assertWithMessage("Config hash key should not be null")
164             .that(hash)
165             .isNotNull();
166 
167         cache.reset();
168 
169         assertWithMessage("Invalid config hash key")
170             .that(cache.get(PropertyCacheFile.CONFIG_HASH_KEY))
171             .isEqualTo(hash);
172     }
173 
174     @Test
175     public void testConfigHashRemainsOnResetExternalResources() throws IOException {
176         final Configuration config = new DefaultConfiguration("myName");
177         final String uniqueFileName = "file_" + UUID.randomUUID() + ".java";
178         final File filePath = new File(temporaryFolder, uniqueFileName);
179         final PropertyCacheFile cache = new PropertyCacheFile(config, filePath.toString());
180 
181         // create cache with one file
182         cache.load();
183         cache.put("myFile", 1);
184 
185         final String hash = cache.get(PropertyCacheFile.CONFIG_HASH_KEY);
186         assertWithMessage("Config hash key should not be null")
187             .that(hash)
188             .isNotNull();
189 
190         // apply new external resource to clear cache
191         final Set<String> resources = new HashSet<>();
192         resources.add("dummy");
193         cache.putExternalResources(resources);
194 
195         assertWithMessage("Invalid config hash key")
196             .that(cache.get(PropertyCacheFile.CONFIG_HASH_KEY))
197             .isEqualTo(hash);
198         assertWithMessage("Should return false in file is not in cache")
199                 .that(cache.isInCache("myFile", 1))
200                 .isFalse();
201     }
202 
203     @Test
204     public void testCacheRemainsWhenExternalResourceTheSame() throws IOException {
205         final Configuration config = new DefaultConfiguration("myName");
206         final String externalFile = "junit_" + UUID.randomUUID() + ".java";
207         final File externalResourcePath = new File(temporaryFolder, externalFile);
208         externalResourcePath.createNewFile();
209         final String uniqueFileName = "junit_" + UUID.randomUUID() + ".java";
210         final File filePath = new File(temporaryFolder, uniqueFileName);
211         filePath.createNewFile();
212         final PropertyCacheFile cache = new PropertyCacheFile(config, filePath.toString());
213 
214         // pre-populate with cache with resources
215 
216         cache.load();
217 
218         final Set<String> resources = new HashSet<>();
219         resources.add(externalResourcePath.toString());
220         cache.putExternalResources(resources);
221 
222         cache.persist();
223 
224         // test cache with same resources and new file
225 
226         cache.load();
227         cache.put("myFile", 1);
228         cache.putExternalResources(resources);
229 
230         assertWithMessage("Should return true in file is in cache")
231                 .that(cache.isInCache("myFile", 1))
232                 .isTrue();
233     }
234 
235     @Test
236     public void testExternalResourceIsSavedInCache() throws Exception {
237         final Configuration config = new DefaultConfiguration("myName");
238         final String uniqueFileName = "junit_" + UUID.randomUUID() + ".java";
239         final File filePath = new File(temporaryFolder, uniqueFileName);
240         final PropertyCacheFile cache = new PropertyCacheFile(config, filePath.toString());
241 
242         cache.load();
243 
244         final Set<String> resources = new HashSet<>();
245         final String pathToResource = getPath("InputPropertyCacheFileExternal.properties");
246         resources.add(pathToResource);
247         cache.putExternalResources(resources);
248 
249         final MessageDigest digest = MessageDigest.getInstance("SHA-1");
250         final URI uri = CommonUtil.getUriByFilename(pathToResource);
251         final byte[] input =
252                 ByteStreams.toByteArray(new BufferedInputStream(uri.toURL().openStream()));
253         final ByteArrayOutputStream out = new ByteArrayOutputStream();
254         try (ObjectOutputStream oos = new ObjectOutputStream(out)) {
255             oos.writeObject(input);
256         }
257         digest.update(out.toByteArray());
258         final String expected = BaseEncoding.base16().upperCase().encode(digest.digest());
259 
260         assertWithMessage("Hashes are not equal")
261             .that(cache.get("module-resource*?:" + pathToResource))
262             .isEqualTo(expected);
263     }
264 
265     @Test
266     public void testCacheDirectoryDoesNotExistAndShouldBeCreated() throws IOException {
267         final Configuration config = new DefaultConfiguration("myName");
268         final String filePath = String.format(Locale.ENGLISH, "%s%2$stemp%2$scache.temp",
269             temporaryFolder, File.separator);
270         final PropertyCacheFile cache = new PropertyCacheFile(config, filePath);
271 
272         // no exception expected, cache directory should be created
273         cache.persist();
274 
275         assertWithMessage("cache exists in directory")
276                 .that(new File(filePath).exists())
277                 .isTrue();
278     }
279 
280     @Test
281     public void testPathToCacheContainsOnlyFileName() throws IOException {
282         final Configuration config = new DefaultConfiguration("myName");
283         final String fileName = "temp.cache";
284         final Path filePath = Path.of(fileName);
285         final PropertyCacheFile cache = new PropertyCacheFile(config, fileName);
286 
287         deleteDirectoryRecursively(filePath);
288 
289         // no exception expected
290         cache.persist();
291 
292         assertWithMessage("Cache file does not exist")
293                 .that(Files.exists(filePath))
294                 .isTrue();
295         Files.delete(filePath);
296     }
297 
298     @Test
299     @DisabledOnOs(OS.WINDOWS)
300     public void testPersistWithSymbolicLinkToDirectory() throws IOException {
301         final Path tempDirectory = temporaryFolder.toPath();
302         final Path symbolicLinkDirectory = temporaryFolder.toPath()
303                 .resolve("symbolicLink");
304         Files.createSymbolicLink(symbolicLinkDirectory, tempDirectory);
305 
306         final Configuration config = new DefaultConfiguration("myName");
307         final String cacheFilePath = symbolicLinkDirectory.resolve("cache.temp").toString();
308         final PropertyCacheFile cache = new PropertyCacheFile(config, cacheFilePath);
309 
310         cache.persist();
311 
312         final Path expectedFilePath = tempDirectory.resolve("cache.temp");
313         assertWithMessage("Cache file should be created in the actual directory")
314                 .that(Files.exists(expectedFilePath))
315                 .isTrue();
316     }
317 
318     @Test
319     @DisabledOnOs(OS.WINDOWS)
320     public void testSymbolicLinkResolution() throws IOException {
321         final Path tempDirectory = temporaryFolder.toPath();
322         final Path symbolicLinkDirectory = temporaryFolder.toPath()
323                 .resolve("symbolicLink");
324         Files.createSymbolicLink(symbolicLinkDirectory, tempDirectory);
325 
326         final Configuration config = new DefaultConfiguration("myName");
327         final String cacheFilePath = symbolicLinkDirectory.resolve("cache.temp").toString();
328         final PropertyCacheFile cache = new PropertyCacheFile(config, cacheFilePath);
329 
330         cache.persist();
331 
332         final Path expectedFilePath = tempDirectory.resolve("cache.temp");
333         assertWithMessage(
334                 "Cache file should be created in the actual directory.")
335                 .that(Files.exists(expectedFilePath))
336                 .isTrue();
337     }
338 
339     @Test
340     @DisabledOnOs(OS.WINDOWS)
341     public void testSymbolicLinkToNonDirectory() throws IOException {
342         final String uniqueFileName = "tempFile_" + UUID.randomUUID() + ".java";
343         final File tempFile = new File(temporaryFolder, uniqueFileName);
344         tempFile.createNewFile();
345         final Path symbolicLinkDirectory = temporaryFolder.toPath();
346         final Path symbolicLink = symbolicLinkDirectory.resolve("symbolicLink");
347         Files.createSymbolicLink(symbolicLink, tempFile.toPath());
348 
349         final Configuration config = new DefaultConfiguration("myName");
350         final String cacheFilePath = symbolicLink.resolve("cache.temp").toString();
351         final PropertyCacheFile cache = new PropertyCacheFile(config, cacheFilePath);
352 
353         final IOException thrown = getExpectedThrowable(IOException.class, cache::persist);
354 
355         final String expectedMessage = "Resolved symbolic link " + symbolicLink
356                 + " is not a directory.";
357 
358         assertWithMessage(
359                 "Expected IOException when symbolicLink is not a directory")
360                 .that(thrown.getMessage())
361                 .contains(expectedMessage);
362     }
363 
364     @Test
365     @DisabledOnOs(OS.WINDOWS)
366     public void testMultipleSymbolicLinkResolution() throws IOException {
367         final Path actualDirectory = temporaryFolder.toPath();
368         final Path firstSymbolicLink = temporaryFolder.toPath()
369                 .resolve("firstLink");
370         Files.createSymbolicLink(firstSymbolicLink, actualDirectory);
371 
372         final Path secondSymbolicLink = temporaryFolder.toPath()
373                 .resolve("secondLink");
374         Files.createSymbolicLink(secondSymbolicLink, firstSymbolicLink);
375 
376         final Configuration config = new DefaultConfiguration("myName");
377         final String cacheFilePath = secondSymbolicLink.resolve("cache.temp").toString();
378         final PropertyCacheFile cache = new PropertyCacheFile(config, cacheFilePath);
379 
380         cache.persist();
381 
382         final Path expectedFilePath = actualDirectory.resolve("cache.temp");
383         assertWithMessage("Cache file should be created in the final actual directory")
384                 .that(Files.exists(expectedFilePath))
385                 .isTrue();
386     }
387 
388     @Test
389     public void testChangeInConfig() throws Exception {
390         final DefaultConfiguration config = new DefaultConfiguration("myConfig");
391         config.addProperty("attr", "value");
392         final String uniqueFileName = "junit_" + UUID.randomUUID() + ".java";
393         final File cacheFile = new File(temporaryFolder, uniqueFileName);
394         final PropertyCacheFile cache = new PropertyCacheFile(config, cacheFile.getPath());
395         cache.load();
396 
397         final String expectedInitialConfigHash = "D5BB1747FC11B2BB839C80A6C28E3E7684AF9940";
398         final String actualInitialConfigHash = cache.get(PropertyCacheFile.CONFIG_HASH_KEY);
399         assertWithMessage("Invalid config hash")
400             .that(actualInitialConfigHash)
401             .isEqualTo(expectedInitialConfigHash);
402 
403         cache.persist();
404 
405         final Properties details = new Properties();
406         try (BufferedReader reader = Files.newBufferedReader(cacheFile.toPath())) {
407             details.load(reader);
408         }
409         assertWithMessage("Invalid details size")
410             .that(details)
411             .hasSize(1);
412 
413         // change in config
414         config.addProperty("newAttr", "newValue");
415 
416         final PropertyCacheFile cacheAfterChangeInConfig =
417             new PropertyCacheFile(config, cacheFile.getPath());
418         cacheAfterChangeInConfig.load();
419 
420         final String expectedConfigHashAfterChange = "714876AE38C069EC52BF86889F061B3776E526D3";
421         final String actualConfigHashAfterChange =
422             cacheAfterChangeInConfig.get(PropertyCacheFile.CONFIG_HASH_KEY);
423         assertWithMessage("Invalid config hash")
424             .that(actualConfigHashAfterChange)
425             .isEqualTo(expectedConfigHashAfterChange);
426 
427         cacheAfterChangeInConfig.persist();
428 
429         final Properties detailsAfterChangeInConfig = new Properties();
430         try (BufferedReader reader = Files.newBufferedReader(cacheFile.toPath())) {
431             detailsAfterChangeInConfig.load(reader);
432         }
433         assertWithMessage("Invalid cache size")
434             .that(detailsAfterChangeInConfig)
435             .hasSize(1);
436     }
437 
438     @Test
439     public void testNonExistentResource() throws IOException {
440         final Configuration config = new DefaultConfiguration("myName");
441         final String uniqueFileName = "junit_" + UUID.randomUUID() + ".java";
442         final File filePath = new File(temporaryFolder, uniqueFileName);
443         final PropertyCacheFile cache = new PropertyCacheFile(config, filePath.toString());
444 
445         // create cache with one file
446         cache.load();
447         final String myFile = "myFile";
448         cache.put(myFile, 1);
449 
450         final String hash = cache.get(PropertyCacheFile.CONFIG_HASH_KEY);
451         assertWithMessage("Config hash key should not be null")
452                 .that(hash)
453                 .isNotNull();
454 
455         // apply new external resource to clear cache
456         final Set<String> resources = new HashSet<>();
457         final String resource = getPath("InputPropertyCacheFile.header");
458         resources.add(resource);
459         cache.putExternalResources(resources);
460 
461         assertWithMessage("Should return false in file is not in cache")
462                 .that(cache.isInCache(myFile, 1))
463                 .isFalse();
464 
465         assertWithMessage("Should return false in file is not in cache")
466                 .that(cache.isInCache(resource, 1))
467                 .isFalse();
468     }
469 
470     @Test
471     public void testExceptionNoSuchAlgorithmException() {
472         final Configuration config = new DefaultConfiguration("myName");
473         final String uniqueFileName = "junit_" + UUID.randomUUID() + ".java";
474         final File filePath = new File(temporaryFolder, uniqueFileName);
475         final PropertyCacheFile cache = new PropertyCacheFile(config, filePath.toString());
476         cache.put("myFile", 1);
477 
478         try (MockedStatic<MessageDigest> messageDigest = mockStatic(MessageDigest.class)) {
479             messageDigest.when(() -> MessageDigest.getInstance("SHA-1"))
480                     .thenThrow(NoSuchAlgorithmException.class);
481 
482             final ReflectiveOperationException ex =
483                 getExpectedThrowable(ReflectiveOperationException.class, () -> {
484                     TestUtil.invokeStaticMethod(PropertyCacheFile.class,
485                             "getHashCodeBasedOnObjectContent", config);
486                 });
487             assertWithMessage("Invalid exception cause")
488                 .that(ex)
489                     .hasCauseThat()
490                         .hasCauseThat()
491                         .isInstanceOf(NoSuchAlgorithmException.class);
492             assertWithMessage("Invalid exception message")
493                 .that(ex)
494                     .hasCauseThat()
495                         .hasMessageThat()
496                         .isEqualTo("Unable to calculate hashcode.");
497         }
498     }
499 
500     /**
501      * Recursively deletes the specified directory and all its contents.
502      *
503      * <p>
504      * This method walks through all files and subdirectories inside the given directory,
505      * and deletes them in reverse order (children before parents) to ensure complete deletion.
506      * It throws an {@link IOException} if any file or directory cannot be deleted.
507      * </p>
508      *
509      * @param directory the path to the directory to be deleted
510      * @throws IOException if an I/O error occurs during walking or deleting the directory
511      */
512     private static void deleteDirectoryRecursively(Path directory) throws IOException {
513         if (Files.exists(directory) && Files.isDirectory(directory)) {
514             // collect all paths inside the directory
515             try (Stream<Path> walkedPathsStream = Files.walk(directory)
516                         .sorted(Comparator.reverseOrder())) {
517                 final Iterator<Path> iterator = walkedPathsStream.iterator();
518                 while (iterator.hasNext()) {
519                     Files.delete(iterator.next());
520                 }
521             }
522         }
523     }
524 
525     /**
526      * This test invokes {@code putExternalResources} twice to invalidate cache.
527      * And asserts that two different exceptions produces different content,
528      * but two exceptions with same message produces one shared content.
529      *
530      * @param rawMessages exception messages separated by ';'
531      */
532     @ParameterizedTest
533     @ValueSource(strings = {"Same;Same", "First;Second"})
534     public void testPutNonExistentExternalResource(String rawMessages) throws Exception {
535         final String uniqueFileName = "junit_" + UUID.randomUUID() + ".java";
536         final File cacheFile = new File(temporaryFolder, uniqueFileName);
537         final String[] messages = rawMessages.split(";");
538         // We mock getUriByFilename method of CommonUtil to guarantee that it will
539         // throw CheckstyleException with the specific content.
540         try (MockedStatic<CommonUtil> commonUtil = mockStatic(CommonUtil.class)) {
541             final int numberOfRuns = messages.length;
542             final String[] configHashes = new String[numberOfRuns];
543             final String[] externalResourceHashes = new String[numberOfRuns];
544             for (int i = 0; i < numberOfRuns; i++) {
545                 commonUtil.when(() -> CommonUtil.getUriByFilename(any(String.class)))
546                         .thenThrow(new CheckstyleException(messages[i]));
547                 final Configuration config = new DefaultConfiguration("myConfig");
548                 final PropertyCacheFile cache = new PropertyCacheFile(config, cacheFile.getPath());
549                 cache.load();
550 
551                 configHashes[i] = cache.get(PropertyCacheFile.CONFIG_HASH_KEY);
552                 assertWithMessage("Config hash key should not be null")
553                         .that(configHashes[i])
554                         .isNotNull();
555 
556                 final Set<String> nonExistentExternalResources = new HashSet<>();
557                 final String externalResourceFileName = "non_existent_file.xml";
558                 nonExistentExternalResources.add(externalResourceFileName);
559                 cache.putExternalResources(nonExistentExternalResources);
560 
561                 externalResourceHashes[i] = cache.get(PropertyCacheFile.EXTERNAL_RESOURCE_KEY_PREFIX
562                         + externalResourceFileName);
563                 assertWithMessage("External resource hashes should not be null")
564                         .that(externalResourceHashes[i])
565                         .isNotNull();
566 
567                 cache.persist();
568 
569                 final Properties cacheDetails = new Properties();
570                 try (BufferedReader reader = Files.newBufferedReader(cacheFile.toPath())) {
571                     cacheDetails.load(reader);
572                 }
573 
574                 assertWithMessage("Unexpected number of objects in cache")
575                         .that(cacheDetails)
576                         .hasSize(2);
577             }
578 
579             assertWithMessage("Invalid config hash")
580                     .that(configHashes[0])
581                     .isEqualTo(configHashes[1]);
582             final boolean sameException = messages[0].equals(messages[1]);
583             assertWithMessage("Invalid external resource hashes")
584                     .that(externalResourceHashes[0].equals(externalResourceHashes[1]))
585                     .isEqualTo(sameException);
586         }
587     }
588 
589 }