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