1 use super::*;
2 use crate::config::tests::test_prolog;
3 use more_asserts::{assert_ge, assert_gt, assert_lt};
4 use std::iter::repeat;
5 use std::process;
6 // load_config! comes from crate::cache(::config::tests);
7 
8 // when doing anything with the tests, make sure they are DETERMINISTIC
9 // -- the result shouldn't rely on system time!
10 pub mod system_time_stub;
11 
12 #[test]
13 fn test_on_get_create_stats_file() {
14     let (_tempdir, cache_dir, config_path) = test_prolog();
15     let cache_config = load_config!(
16         config_path,
17         "[cache]\n\
18          enabled = true\n\
19          directory = {cache_dir}",
20         cache_dir
21     );
22     assert!(cache_config.enabled());
23     let worker = Worker::start_new(&cache_config, None);
24 
25     let mod_file = cache_dir.join("some-mod");
26     worker.on_cache_get_async(mod_file);
27     worker.wait_for_all_events_handled();
28     assert_eq!(worker.events_dropped(), 0);
29 
30     let stats_file = cache_dir.join("some-mod.stats");
31     let stats = read_stats_file(&stats_file).expect("Failed to read stats file");
32     assert_eq!(stats.usages, 1);
33     assert_eq!(
34         stats.compression_level,
35         cache_config.baseline_compression_level()
36     );
37 }
38 
39 #[test]
40 fn test_on_get_update_usage_counter() {
41     let (_tempdir, cache_dir, config_path) = test_prolog();
42     let cache_config = load_config!(
43         config_path,
44         "[cache]\n\
45          enabled = true\n\
46          directory = {cache_dir}\n\
47          worker-event-queue-size = '16'",
48         cache_dir
49     );
50     assert!(cache_config.enabled());
51     let worker = Worker::start_new(&cache_config, None);
52 
53     let mod_file = cache_dir.join("some-mod");
54     let stats_file = cache_dir.join("some-mod.stats");
55     let default_stats = ModuleCacheStatistics::default(&cache_config);
56     assert!(write_stats_file(&stats_file, &default_stats));
57 
58     let mut usages = 0;
59     for times_used in &[4, 7, 2] {
60         for _ in 0..*times_used {
61             worker.on_cache_get_async(mod_file.clone());
62             usages += 1;
63         }
64 
65         worker.wait_for_all_events_handled();
66         assert_eq!(worker.events_dropped(), 0);
67 
68         let stats = read_stats_file(&stats_file).expect("Failed to read stats file");
69         assert_eq!(stats.usages, usages);
70     }
71 }
72 
73 #[test]
74 fn test_on_get_recompress_no_mod_file() {
75     let (_tempdir, cache_dir, config_path) = test_prolog();
76     let cache_config = load_config!(
77         config_path,
78         "[cache]\n\
79          enabled = true\n\
80          directory = {cache_dir}\n\
81          worker-event-queue-size = '16'\n\
82          baseline-compression-level = 3\n\
83          optimized-compression-level = 7\n\
84          optimized-compression-usage-counter-threshold = '256'",
85         cache_dir
86     );
87     assert!(cache_config.enabled());
88     let worker = Worker::start_new(&cache_config, None);
89 
90     let mod_file = cache_dir.join("some-mod");
91     let stats_file = cache_dir.join("some-mod.stats");
92     let mut start_stats = ModuleCacheStatistics::default(&cache_config);
93     start_stats.usages = 250;
94     assert!(write_stats_file(&stats_file, &start_stats));
95 
96     let mut usages = start_stats.usages;
97     for times_used in &[4, 7, 2] {
98         for _ in 0..*times_used {
99             worker.on_cache_get_async(mod_file.clone());
100             usages += 1;
101         }
102 
103         worker.wait_for_all_events_handled();
104         assert_eq!(worker.events_dropped(), 0);
105 
106         let stats = read_stats_file(&stats_file).expect("Failed to read stats file");
107         assert_eq!(stats.usages, usages);
108         assert_eq!(
109             stats.compression_level,
110             cache_config.baseline_compression_level()
111         );
112     }
113 }
114 
115 #[test]
116 fn test_on_get_recompress_with_mod_file() {
117     let (_tempdir, cache_dir, config_path) = test_prolog();
118     let cache_config = load_config!(
119         config_path,
120         "[cache]\n\
121          enabled = true\n\
122          directory = {cache_dir}\n\
123          worker-event-queue-size = '16'\n\
124          baseline-compression-level = 3\n\
125          optimized-compression-level = 7\n\
126          optimized-compression-usage-counter-threshold = '256'",
127         cache_dir
128     );
129     assert!(cache_config.enabled());
130     let worker = Worker::start_new(&cache_config, None);
131 
132     let mod_file = cache_dir.join("some-mod");
133     let mod_data = "some test data to be compressed";
134     let data = zstd::encode_all(
135         mod_data.as_bytes(),
136         cache_config.baseline_compression_level(),
137     )
138     .expect("Failed to compress sample mod file");
139     fs::write(&mod_file, &data).expect("Failed to write sample mod file");
140 
141     let stats_file = cache_dir.join("some-mod.stats");
142     let mut start_stats = ModuleCacheStatistics::default(&cache_config);
143     start_stats.usages = 250;
144     assert!(write_stats_file(&stats_file, &start_stats));
145 
146     // scenarios:
147     // 1. Shouldn't be recompressed
148     // 2. Should be recompressed
149     // 3. After lowering compression level, should be recompressed
150     let scenarios = [(4, false), (7, true), (2, false)];
151 
152     let mut usages = start_stats.usages;
153     assert_lt!(
154         usages,
155         cache_config.optimized_compression_usage_counter_threshold()
156     );
157     let mut tested_higher_opt_compr_lvl = false;
158     for (times_used, lower_compr_lvl) in &scenarios {
159         for _ in 0..*times_used {
160             worker.on_cache_get_async(mod_file.clone());
161             usages += 1;
162         }
163 
164         worker.wait_for_all_events_handled();
165         assert_eq!(worker.events_dropped(), 0);
166 
167         let mut stats = read_stats_file(&stats_file).expect("Failed to read stats file");
168         assert_eq!(stats.usages, usages);
169         assert_eq!(
170             stats.compression_level,
171             if usages < cache_config.optimized_compression_usage_counter_threshold() {
172                 cache_config.baseline_compression_level()
173             } else {
174                 cache_config.optimized_compression_level()
175             }
176         );
177         let compressed_data = fs::read(&mod_file).expect("Failed to read mod file");
178         let decoded_data =
179             zstd::decode_all(&compressed_data[..]).expect("Failed to decompress mod file");
180         assert_eq!(decoded_data, mod_data.as_bytes());
181 
182         if *lower_compr_lvl {
183             assert_ge!(
184                 usages,
185                 cache_config.optimized_compression_usage_counter_threshold()
186             );
187             tested_higher_opt_compr_lvl = true;
188             stats.compression_level -= 1;
189             assert!(write_stats_file(&stats_file, &stats));
190         }
191     }
192     assert_ge!(
193         usages,
194         cache_config.optimized_compression_usage_counter_threshold()
195     );
196     assert!(tested_higher_opt_compr_lvl);
197 }
198 
199 #[test]
200 fn test_on_get_recompress_lock() {
201     let (_tempdir, cache_dir, config_path) = test_prolog();
202     let cache_config = load_config!(
203         config_path,
204         "[cache]\n\
205          enabled = true\n\
206          directory = {cache_dir}\n\
207          worker-event-queue-size = '16'\n\
208          baseline-compression-level = 3\n\
209          optimized-compression-level = 7\n\
210          optimized-compression-usage-counter-threshold = '256'\n\
211          optimizing-compression-task-timeout = '30m'\n\
212          allowed-clock-drift-for-files-from-future = '1d'",
213         cache_dir
214     );
215     assert!(cache_config.enabled());
216     let worker = Worker::start_new(&cache_config, None);
217 
218     let mod_file = cache_dir.join("some-mod");
219     let mod_data = "some test data to be compressed";
220     let data = zstd::encode_all(
221         mod_data.as_bytes(),
222         cache_config.baseline_compression_level(),
223     )
224     .expect("Failed to compress sample mod file");
225     fs::write(&mod_file, &data).expect("Failed to write sample mod file");
226 
227     let stats_file = cache_dir.join("some-mod.stats");
228     let mut start_stats = ModuleCacheStatistics::default(&cache_config);
229     start_stats.usages = 255;
230 
231     let lock_file = cache_dir.join("some-mod.wip-lock");
232 
233     let scenarios = [
234         // valid lock
235         (true, "past", Duration::from_secs(30 * 60 - 1)),
236         // valid future lock
237         (true, "future", Duration::from_secs(24 * 60 * 60)),
238         // expired lock
239         (false, "past", Duration::from_secs(30 * 60)),
240         // expired future lock
241         (false, "future", Duration::from_secs(24 * 60 * 60 + 1)),
242     ];
243 
244     for (lock_valid, duration_sign, duration) in &scenarios {
245         assert!(write_stats_file(&stats_file, &start_stats)); // restore usage & compression level
246         create_file_with_mtime(&lock_file, "", duration_sign, &duration);
247 
248         worker.on_cache_get_async(mod_file.clone());
249         worker.wait_for_all_events_handled();
250         assert_eq!(worker.events_dropped(), 0);
251 
252         let stats = read_stats_file(&stats_file).expect("Failed to read stats file");
253         assert_eq!(stats.usages, start_stats.usages + 1);
254         assert_eq!(
255             stats.compression_level,
256             if *lock_valid {
257                 cache_config.baseline_compression_level()
258             } else {
259                 cache_config.optimized_compression_level()
260             }
261         );
262         let compressed_data = fs::read(&mod_file).expect("Failed to read mod file");
263         let decoded_data =
264             zstd::decode_all(&compressed_data[..]).expect("Failed to decompress mod file");
265         assert_eq!(decoded_data, mod_data.as_bytes());
266     }
267 }
268 
269 #[test]
270 fn test_on_update_fresh_stats_file() {
271     let (_tempdir, cache_dir, config_path) = test_prolog();
272     let cache_config = load_config!(
273         config_path,
274         "[cache]\n\
275          enabled = true\n\
276          directory = {cache_dir}\n\
277          worker-event-queue-size = '16'\n\
278          baseline-compression-level = 3\n\
279          optimized-compression-level = 7\n\
280          cleanup-interval = '1h'",
281         cache_dir
282     );
283     assert!(cache_config.enabled());
284     let worker = Worker::start_new(&cache_config, None);
285 
286     let mod_file = cache_dir.join("some-mod");
287     let stats_file = cache_dir.join("some-mod.stats");
288     let cleanup_certificate = cache_dir.join(".cleanup.wip-done");
289     create_file_with_mtime(&cleanup_certificate, "", "future", &Duration::from_secs(0));
290     // the below created by the worker if it cleans up
291     let worker_lock_file = cache_dir.join(format!(".cleanup.wip-{}", process::id()));
292 
293     // scenarios:
294     // 1. Create new stats file
295     // 2. Overwrite existing file
296     for update_file in &[true, false] {
297         worker.on_cache_update_async(mod_file.clone());
298         worker.wait_for_all_events_handled();
299         assert_eq!(worker.events_dropped(), 0);
300 
301         let mut stats = read_stats_file(&stats_file).expect("Failed to read stats file");
302         assert_eq!(stats.usages, 1);
303         assert_eq!(
304             stats.compression_level,
305             cache_config.baseline_compression_level()
306         );
307 
308         if *update_file {
309             stats.usages += 42;
310             stats.compression_level += 1;
311             assert!(write_stats_file(&stats_file, &stats));
312         }
313 
314         assert!(!worker_lock_file.exists());
315     }
316 }
317 
318 #[test]
319 fn test_on_update_cleanup_limits_trash_locks() {
320     let (_tempdir, cache_dir, config_path) = test_prolog();
321     let cache_config = load_config!(
322         config_path,
323         "[cache]\n\
324          enabled = true\n\
325          directory = {cache_dir}\n\
326          worker-event-queue-size = '16'\n\
327          cleanup-interval = '30m'\n\
328          optimizing-compression-task-timeout = '30m'\n\
329          allowed-clock-drift-for-files-from-future = '1d'\n\
330          file-count-soft-limit = '5'\n\
331          files-total-size-soft-limit = '30K'\n\
332          file-count-limit-percent-if-deleting = '70%'\n\
333          files-total-size-limit-percent-if-deleting = '70%'
334          ",
335         cache_dir
336     );
337     assert!(cache_config.enabled());
338     let worker = Worker::start_new(&cache_config, None);
339     let content_1k = "a".repeat(1_000);
340     let content_10k = "a".repeat(10_000);
341 
342     let mods_files_dir = cache_dir.join("target-triple").join("compiler-version");
343     let mod_with_stats = mods_files_dir.join("mod-with-stats");
344     let trash_dirs = [
345         mods_files_dir.join("trash"),
346         mods_files_dir.join("trash").join("trash"),
347     ];
348     let trash_files = [
349         cache_dir.join("trash-file"),
350         cache_dir.join("trash-file.wip-lock"),
351         cache_dir.join("target-triple").join("trash.txt"),
352         cache_dir.join("target-triple").join("trash.txt.wip-lock"),
353         mods_files_dir.join("trash.ogg"),
354         mods_files_dir.join("trash").join("trash.doc"),
355         mods_files_dir.join("trash").join("trash.doc.wip-lock"),
356         mods_files_dir.join("trash").join("trash").join("trash.xls"),
357         mods_files_dir
358             .join("trash")
359             .join("trash")
360             .join("trash.xls.wip-lock"),
361     ];
362     let mod_locks = [
363         // valid lock
364         (
365             mods_files_dir.join("mod0.wip-lock"),
366             true,
367             "past",
368             Duration::from_secs(30 * 60 - 1),
369         ),
370         // valid future lock
371         (
372             mods_files_dir.join("mod1.wip-lock"),
373             true,
374             "future",
375             Duration::from_secs(24 * 60 * 60),
376         ),
377         // expired lock
378         (
379             mods_files_dir.join("mod2.wip-lock"),
380             false,
381             "past",
382             Duration::from_secs(30 * 60),
383         ),
384         // expired future lock
385         (
386             mods_files_dir.join("mod3.wip-lock"),
387             false,
388             "future",
389             Duration::from_secs(24 * 60 * 60 + 1),
390         ),
391     ];
392     // the below created by the worker if it cleans up
393     let worker_lock_file = cache_dir.join(format!(".cleanup.wip-{}", process::id()));
394 
395     let scenarios = [
396         // Close to limits, but not reached, only trash deleted
397         (2, 2, 4),
398         // File count limit exceeded
399         (1, 10, 3),
400         // Total size limit exceeded
401         (4, 0, 2),
402         // Both limits exceeded
403         (3, 5, 3),
404     ];
405 
406     for (files_10k, files_1k, remaining_files) in &scenarios {
407         let mut secs_ago = 100;
408 
409         for d in &trash_dirs {
410             fs::create_dir_all(d).expect("Failed to create directories");
411         }
412         for f in &trash_files {
413             create_file_with_mtime(f, "", "past", &Duration::from_secs(0));
414         }
415         for (f, _, sign, duration) in &mod_locks {
416             create_file_with_mtime(f, "", sign, &duration);
417         }
418 
419         let mut mods_paths = vec![];
420         for content in repeat(&content_10k)
421             .take(*files_10k)
422             .chain(repeat(&content_1k).take(*files_1k))
423         {
424             mods_paths.push(mods_files_dir.join(format!("test-mod-{}", mods_paths.len())));
425             create_file_with_mtime(
426                 mods_paths.last().unwrap(),
427                 content,
428                 "past",
429                 &Duration::from_secs(secs_ago),
430             );
431             assert_gt!(secs_ago, 0);
432             secs_ago -= 1;
433         }
434 
435         // creating .stats file updates mtime what affects test results
436         // so we use a separate nonexistent module here (orphaned .stats will be removed anyway)
437         worker.on_cache_update_async(mod_with_stats.clone());
438         worker.wait_for_all_events_handled();
439         assert_eq!(worker.events_dropped(), 0);
440 
441         for ent in trash_dirs.iter().chain(trash_files.iter()) {
442             assert!(!ent.exists());
443         }
444         for (f, valid, ..) in &mod_locks {
445             assert_eq!(f.exists(), *valid);
446         }
447         for (idx, path) in mods_paths.iter().enumerate() {
448             let should_exist = idx >= mods_paths.len() - *remaining_files;
449             assert_eq!(path.exists(), should_exist);
450             if should_exist {
451                 // cleanup before next iteration
452                 fs::remove_file(path).expect("Failed to remove a file");
453             }
454         }
455         fs::remove_file(&worker_lock_file).expect("Failed to remove lock file");
456     }
457 }
458 
459 #[test]
460 fn test_on_update_cleanup_lru_policy() {
461     let (_tempdir, cache_dir, config_path) = test_prolog();
462     let cache_config = load_config!(
463         config_path,
464         "[cache]\n\
465          enabled = true\n\
466          directory = {cache_dir}\n\
467          worker-event-queue-size = '16'\n\
468          file-count-soft-limit = '5'\n\
469          files-total-size-soft-limit = '30K'\n\
470          file-count-limit-percent-if-deleting = '80%'\n\
471          files-total-size-limit-percent-if-deleting = '70%'",
472         cache_dir
473     );
474     assert!(cache_config.enabled());
475     let worker = Worker::start_new(&cache_config, None);
476     let content_1k = "a".repeat(1_000);
477     let content_5k = "a".repeat(5_000);
478     let content_10k = "a".repeat(10_000);
479 
480     let mods_files_dir = cache_dir.join("target-triple").join("compiler-version");
481     fs::create_dir_all(&mods_files_dir).expect("Failed to create directories");
482     let nonexistent_mod_file = cache_dir.join("nonexistent-mod");
483     let orphaned_stats_file = cache_dir.join("orphaned-mod.stats");
484     let worker_lock_file = cache_dir.join(format!(".cleanup.wip-{}", process::id()));
485 
486     // content, how long ago created, how long ago stats created (if created), should be alive
487     let scenarios = [
488         &[
489             (&content_10k, 29, None, false),
490             (&content_10k, 28, None, false),
491             (&content_10k, 27, None, false),
492             (&content_1k, 26, None, true),
493             (&content_10k, 25, None, true),
494             (&content_1k, 24, None, true),
495         ],
496         &[
497             (&content_10k, 29, None, false),
498             (&content_10k, 28, None, false),
499             (&content_10k, 27, None, true),
500             (&content_1k, 26, None, true),
501             (&content_5k, 25, None, true),
502             (&content_1k, 24, None, true),
503         ],
504         &[
505             (&content_10k, 29, Some(19), true),
506             (&content_10k, 28, None, false),
507             (&content_10k, 27, None, false),
508             (&content_1k, 26, Some(18), true),
509             (&content_5k, 25, None, true),
510             (&content_1k, 24, None, true),
511         ],
512         &[
513             (&content_10k, 29, Some(19), true),
514             (&content_10k, 28, Some(18), true),
515             (&content_10k, 27, None, false),
516             (&content_1k, 26, Some(17), true),
517             (&content_5k, 25, None, false),
518             (&content_1k, 24, None, false),
519         ],
520         &[
521             (&content_10k, 29, Some(19), true),
522             (&content_10k, 28, None, false),
523             (&content_1k, 27, None, false),
524             (&content_5k, 26, Some(18), true),
525             (&content_1k, 25, None, false),
526             (&content_10k, 24, None, false),
527         ],
528     ];
529 
530     for mods in &scenarios {
531         let filenames = (0..mods.len())
532             .map(|i| {
533                 (
534                     mods_files_dir.join(format!("mod-{}", i)),
535                     mods_files_dir.join(format!("mod-{}.stats", i)),
536                 )
537             })
538             .collect::<Vec<_>>();
539 
540         for ((content, mod_secs_ago, create_stats, _), (mod_filename, stats_filename)) in
541             mods.iter().zip(filenames.iter())
542         {
543             create_file_with_mtime(
544                 mod_filename,
545                 content,
546                 "past",
547                 &Duration::from_secs(*mod_secs_ago),
548             );
549             if let Some(stats_secs_ago) = create_stats {
550                 create_file_with_mtime(
551                     stats_filename,
552                     "cleanup doesn't care",
553                     "past",
554                     &Duration::from_secs(*stats_secs_ago),
555                 );
556             }
557         }
558         create_file_with_mtime(
559             &orphaned_stats_file,
560             "cleanup doesn't care",
561             "past",
562             &Duration::from_secs(0),
563         );
564 
565         worker.on_cache_update_async(nonexistent_mod_file.clone());
566         worker.wait_for_all_events_handled();
567         assert_eq!(worker.events_dropped(), 0);
568 
569         assert!(!orphaned_stats_file.exists());
570         for ((_, _, create_stats, alive), (mod_filename, stats_filename)) in
571             mods.iter().zip(filenames.iter())
572         {
573             assert_eq!(mod_filename.exists(), *alive);
574             assert_eq!(stats_filename.exists(), *alive && create_stats.is_some());
575 
576             // cleanup for next iteration
577             if *alive {
578                 fs::remove_file(&mod_filename).expect("Failed to remove a file");
579                 if create_stats.is_some() {
580                     fs::remove_file(&stats_filename).expect("Failed to remove a file");
581                 }
582             }
583         }
584 
585         fs::remove_file(&worker_lock_file).expect("Failed to remove lock file");
586     }
587 }
588 
589 // clock drift should be applied to mod cache & stats, too
590 // however, postpone deleting files to as late as possible
591 #[test]
592 fn test_on_update_cleanup_future_files() {
593     let (_tempdir, cache_dir, config_path) = test_prolog();
594     let cache_config = load_config!(
595         config_path,
596         "[cache]\n\
597          enabled = true\n\
598          directory = {cache_dir}\n\
599          worker-event-queue-size = '16'\n\
600          allowed-clock-drift-for-files-from-future = '1d'\n\
601          file-count-soft-limit = '3'\n\
602          files-total-size-soft-limit = '1M'\n\
603          file-count-limit-percent-if-deleting = '70%'\n\
604          files-total-size-limit-percent-if-deleting = '70%'",
605         cache_dir
606     );
607     assert!(cache_config.enabled());
608     let worker = Worker::start_new(&cache_config, None);
609     let content_1k = "a".repeat(1_000);
610 
611     let mods_files_dir = cache_dir.join("target-triple").join("compiler-version");
612     fs::create_dir_all(&mods_files_dir).expect("Failed to create directories");
613     let nonexistent_mod_file = cache_dir.join("nonexistent-mod");
614     // the below created by the worker if it cleans up
615     let worker_lock_file = cache_dir.join(format!(".cleanup.wip-{}", process::id()));
616 
617     let scenarios: [&[_]; 5] = [
618         // NOT cleaning up, everythings ok
619         &[
620             (Duration::from_secs(0), None, true),
621             (Duration::from_secs(24 * 60 * 60), None, true),
622         ],
623         // NOT cleaning up, everythings ok
624         &[
625             (Duration::from_secs(0), None, true),
626             (Duration::from_secs(24 * 60 * 60 + 1), None, true),
627         ],
628         // cleaning up, removing files from oldest
629         &[
630             (Duration::from_secs(0), None, false),
631             (Duration::from_secs(24 * 60 * 60), None, true),
632             (Duration::from_secs(1), None, false),
633             (Duration::from_secs(2), None, true),
634         ],
635         // cleaning up, removing files from oldest; deleting file from far future
636         &[
637             (Duration::from_secs(0), None, false),
638             (Duration::from_secs(1), None, true),
639             (Duration::from_secs(24 * 60 * 60 + 1), None, false),
640             (Duration::from_secs(2), None, true),
641         ],
642         // cleaning up, removing files from oldest; file from far future should have .stats from +-now => it's a legitimate file
643         &[
644             (Duration::from_secs(0), None, false),
645             (Duration::from_secs(1), None, false),
646             (
647                 Duration::from_secs(24 * 60 * 60 + 1),
648                 Some(Duration::from_secs(3)),
649                 true,
650             ),
651             (Duration::from_secs(2), None, true),
652         ],
653     ];
654 
655     for mods in &scenarios {
656         let filenames = (0..mods.len())
657             .map(|i| {
658                 (
659                     mods_files_dir.join(format!("mod-{}", i)),
660                     mods_files_dir.join(format!("mod-{}.stats", i)),
661                 )
662             })
663             .collect::<Vec<_>>();
664 
665         for ((duration, opt_stats_duration, _), (mod_filename, stats_filename)) in
666             mods.iter().zip(filenames.iter())
667         {
668             create_file_with_mtime(mod_filename, &content_1k, "future", duration);
669             if let Some(stats_duration) = opt_stats_duration {
670                 create_file_with_mtime(stats_filename, "", "future", stats_duration);
671             }
672         }
673 
674         worker.on_cache_update_async(nonexistent_mod_file.clone());
675         worker.wait_for_all_events_handled();
676         assert_eq!(worker.events_dropped(), 0);
677 
678         for ((_, opt_stats_duration, alive), (mod_filename, stats_filename)) in
679             mods.iter().zip(filenames.iter())
680         {
681             assert_eq!(mod_filename.exists(), *alive);
682             assert_eq!(
683                 stats_filename.exists(),
684                 *alive && opt_stats_duration.is_some()
685             );
686             if *alive {
687                 fs::remove_file(mod_filename).expect("Failed to remove a file");
688                 if opt_stats_duration.is_some() {
689                     fs::remove_file(stats_filename).expect("Failed to remove a file");
690                 }
691             }
692         }
693 
694         fs::remove_file(&worker_lock_file).expect("Failed to remove lock file");
695     }
696 }
697 
698 // this tests if worker triggered cleanup or not when some cleanup lock/certificate was out there
699 #[test]
700 fn test_on_update_cleanup_self_lock() {
701     let (_tempdir, cache_dir, config_path) = test_prolog();
702     let cache_config = load_config!(
703         config_path,
704         "[cache]\n\
705          enabled = true\n\
706          directory = {cache_dir}\n\
707          worker-event-queue-size = '16'\n\
708          cleanup-interval = '30m'\n\
709          allowed-clock-drift-for-files-from-future = '1d'",
710         cache_dir
711     );
712     assert!(cache_config.enabled());
713     let worker = Worker::start_new(&cache_config, None);
714 
715     let mod_file = cache_dir.join("some-mod");
716     let trash_file = cache_dir.join("trash-file.txt");
717 
718     let lock_file = cache_dir.join(".cleanup.wip-lock");
719     // the below created by the worker if it cleans up
720     let worker_lock_file = cache_dir.join(format!(".cleanup.wip-{}", process::id()));
721 
722     let scenarios = [
723         // valid lock
724         (true, "past", Duration::from_secs(30 * 60 - 1)),
725         // valid future lock
726         (true, "future", Duration::from_secs(24 * 60 * 60)),
727         // expired lock
728         (false, "past", Duration::from_secs(30 * 60)),
729         // expired future lock
730         (false, "future", Duration::from_secs(24 * 60 * 60 + 1)),
731     ];
732 
733     for (lock_valid, duration_sign, duration) in &scenarios {
734         create_file_with_mtime(
735             &trash_file,
736             "with trash content",
737             "future",
738             &Duration::from_secs(0),
739         );
740         create_file_with_mtime(&lock_file, "", duration_sign, &duration);
741 
742         worker.on_cache_update_async(mod_file.clone());
743         worker.wait_for_all_events_handled();
744         assert_eq!(worker.events_dropped(), 0);
745 
746         assert_eq!(trash_file.exists(), *lock_valid);
747         assert_eq!(lock_file.exists(), *lock_valid);
748         if *lock_valid {
749             assert!(!worker_lock_file.exists());
750         } else {
751             fs::remove_file(&worker_lock_file).expect("Failed to remove lock file");
752         }
753     }
754 }
755 
756 fn create_file_with_mtime(filename: &Path, contents: &str, offset_sign: &str, offset: &Duration) {
757     fs::write(filename, contents).expect("Failed to create a file");
758     let mtime = match offset_sign {
759         "past" => system_time_stub::NOW
760             .checked_sub(*offset)
761             .expect("Failed to calculate new mtime"),
762         "future" => system_time_stub::NOW
763             .checked_add(*offset)
764             .expect("Failed to calculate new mtime"),
765         _ => unreachable!(),
766     };
767     filetime::set_file_mtime(filename, mtime.into()).expect("Failed to set mtime");
768 }
769