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