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