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