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