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