use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use proptest::prelude::*;
use super::artifacts::{
ArtifactInfo, CrateArtifact, collect_crate_artifacts, parse_crate_artifact_name,
rejuvenate_stale_artifact_mtimes, select_artifacts_for_removal,
};
use super::size::{format_size, parse_size};
fn crate_name_strategy() -> impl Strategy<Value = String> {
"[a-z][a-z0-9_-]{0,30}".prop_map(|s| s.replace('_', "-"))
}
fn hash_strategy() -> impl Strategy<Value = String> {
"[0-9a-f]{16}"
}
#[test]
fn test_parse_crate_artifact_name_basic() {
let cases = vec![
("libfoo-1234567890abcdef", "libfoo", "1234567890abcdef"),
(
"serde-1.0.136-78d1b3f8c7b8e0a2",
"serde-1.0.136",
"78d1b3f8c7b8e0a2",
),
(
"my-cool-lib-0123456789abcdef.rlib",
"my-cool-lib",
"0123456789abcdef",
),
(
"build-script-build-fedcba0987654321",
"build-script-build",
"fedcba0987654321",
),
];
for (input, expected_name, expected_hash) in cases {
let path = Path::new(input);
let result = parse_crate_artifact_name(path);
assert!(result.is_some(), "Failed to parse: {input}");
let (name, hash) = result.unwrap();
assert_eq!(name, expected_name);
assert_eq!(hash, expected_hash);
}
}
#[test]
fn test_parse_crate_artifact_name_invalid() {
let invalid_cases = vec![
"foo", "foo-123", "foo-ghijklmnopqrstuv", "foo-1234567890abcdef0", "-1234567890abcdef", ];
for input in invalid_cases {
let path = Path::new(input);
let result = parse_crate_artifact_name(path);
assert!(result.is_none(), "Should fail to parse: {input}");
}
}
proptest! {
#[test]
fn test_parse_crate_artifact_name_property(
name in crate_name_strategy(),
hash in hash_strategy(),
extension in prop::option::of("[a-z]{1,4}"),
) {
let filename = if let Some(ext) = extension {
format!("{name}-{hash}.{ext}")
} else {
format!("{name}-{hash}")
};
let path = Path::new(&filename);
let result = parse_crate_artifact_name(path);
prop_assert!(result.is_some());
let (parsed_name, parsed_hash) = result.unwrap();
prop_assert_eq!(parsed_name, name);
prop_assert_eq!(parsed_hash, hash);
}
}
fn artifact_info(
path: impl Into<PathBuf>,
size: u64,
mtime: SystemTime,
is_regular_file: bool,
) -> ArtifactInfo {
ArtifactInfo {
path: path.into(),
size,
_modified: mtime,
is_regular_file,
}
}
fn create_test_artifact(name: &str, hash: &str, size: u64, age_days: u64) -> CrateArtifact {
let mtime = SystemTime::now()
.checked_sub(Duration::from_secs(age_days * 24 * 60 * 60))
.unwrap_or(SystemTime::now());
CrateArtifact {
name: name.to_string(),
hash: hash.to_string(),
artifacts: vec![artifact_info(
format!("target/debug/deps/lib{name}-{hash}.rlib"),
size,
mtime,
true,
)],
total_size: size,
newest_mtime: mtime,
}
}
#[test]
fn test_combined_selection_size_and_age() {
let artifacts = vec![
create_test_artifact("old_large", "1234567890abcdef", 5000, 30), create_test_artifact("old_small", "2234567890abcdef", 1000, 20), create_test_artifact("recent_large", "3234567890abcdef", 4000, 5), create_test_artifact("recent_small", "4234567890abcdef", 500, 2), ];
let selected = select_artifacts_for_removal(&artifacts, 10500, Some(6000), 10, None, 0, false);
assert_eq!(selected.len(), 2);
assert!(selected.iter().any(|a| a.name == "old_large"));
assert!(selected.iter().any(|a| a.name == "old_small"));
}
#[test]
fn test_combined_selection_only_age() {
let artifacts = vec![
create_test_artifact("old1", "1234567890abcdef", 1000, 15), create_test_artifact("old2", "2234567890abcdef", 1000, 12), create_test_artifact("new1", "3234567890abcdef", 1000, 5), create_test_artifact("new2", "4234567890abcdef", 1000, 3), ];
let selected = select_artifacts_for_removal(&artifacts, 4000, Some(10000), 10, None, 0, false);
assert_eq!(selected.len(), 2);
assert!(selected.iter().any(|a| a.name == "old1"));
assert!(selected.iter().any(|a| a.name == "old2"));
}
#[test]
fn test_combined_selection_only_size() {
let artifacts = vec![
create_test_artifact("large1", "1234567890abcdef", 5000, 2), create_test_artifact("large2", "2234567890abcdef", 4000, 1), create_test_artifact("small1", "3234567890abcdef", 1000, 3), create_test_artifact("small2", "4234567890abcdef", 500, 2), ];
let selected = select_artifacts_for_removal(&artifacts, 10500, Some(5000), 30, None, 0, false);
assert_eq!(selected.len(), 2);
assert!(selected.iter().any(|a| a.name == "small1"));
assert!(selected.iter().any(|a| a.name == "large1"));
}
#[test]
fn test_combined_selection_no_size_limit() {
let artifacts = vec![
create_test_artifact("old", "1234567890abcdef", 10000, 15),
create_test_artifact("new", "2234567890abcdef", 10000, 5),
];
let selected = select_artifacts_for_removal(&artifacts, 20000, None, 10, None, 0, false);
assert_eq!(selected.len(), 1);
assert_eq!(selected[0].name, "old");
}
#[test]
fn test_combined_selection_everything_removed() {
let artifacts = vec![
create_test_artifact("a", "1234567890abcdef", 5000, 20),
create_test_artifact("b", "2234567890abcdef", 5000, 10),
create_test_artifact("c", "3234567890abcdef", 5000, 5),
];
let selected = select_artifacts_for_removal(&artifacts, 15000, Some(0), 30, None, 0, false);
assert_eq!(selected.len(), 3);
}
#[test]
fn test_combined_selection_exact_size_limit() {
let artifacts = vec![
create_test_artifact("a", "1234567890abcdef", 1000, 15),
create_test_artifact("b", "2234567890abcdef", 2000, 10),
create_test_artifact("c", "3234567890abcdef", 3000, 5),
];
let selected = select_artifacts_for_removal(&artifacts, 6000, Some(6000), 10, None, 0, false);
assert_eq!(selected.len(), 2);
assert!(selected.iter().any(|a| a.name == "a"));
assert!(selected.iter().any(|a| a.name == "b"));
}
#[test]
fn test_combined_selection_zero_age_threshold() {
let artifacts = vec![
create_test_artifact("fresh", "1234567890abcdef", 1000, 0), create_test_artifact("recent", "2234567890abcdef", 2000, 1), create_test_artifact("old", "3234567890abcdef", 3000, 5), ];
let selected = select_artifacts_for_removal(&artifacts, 6000, Some(10000), 0, None, 0, false);
assert_eq!(selected.len(), 3);
}
#[test]
fn test_combined_selection_same_timestamps() {
let now = SystemTime::now()
.checked_sub(Duration::from_secs(15 * 24 * 60 * 60))
.unwrap();
let mut artifacts = vec![
create_test_artifact("a", "1234567890abcdef", 3000, 0),
create_test_artifact("b", "2234567890abcdef", 2000, 0),
create_test_artifact("c", "3234567890abcdef", 1000, 0),
];
for artifact in &mut artifacts {
artifact.newest_mtime = now;
}
let selected = select_artifacts_for_removal(&artifacts, 6000, Some(4000), 10, None, 0, false);
assert!(selected.len() >= 2);
let removed_size: u64 = selected.iter().map(|a| a.total_size).sum();
assert!(removed_size >= 2000); }
#[test]
fn test_combined_selection_empty_list() {
let artifacts = vec![];
let selected = select_artifacts_for_removal(&artifacts, 0, Some(1000), 7, None, 0, false);
assert_eq!(selected.len(), 0);
}
#[test]
fn test_combined_selection_preserves_previous_build_artifacts() {
let now = SystemTime::now();
let one_hour_ago = now.checked_sub(Duration::from_secs(3600)).unwrap();
let one_day_ago = now.checked_sub(Duration::from_secs(24 * 3600)).unwrap();
let mut artifacts = vec![
create_test_artifact("old_artifact", "1111111111111111", 5000, 0), create_test_artifact("recent_artifact1", "2222222222222222", 4000, 0), create_test_artifact("recent_artifact2", "3333333333333333", 3000, 0), create_test_artifact("recent_artifact3", "4444444444444444", 2000, 0), ];
artifacts[0].newest_mtime = one_day_ago; artifacts[1].newest_mtime = one_hour_ago; artifacts[2].newest_mtime = one_hour_ago; artifacts[3].newest_mtime = one_hour_ago;
let previous_build_nanos = one_hour_ago
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos();
let selected = select_artifacts_for_removal(
&artifacts,
14000,
Some(6000),
30, Some(previous_build_nanos),
2, false,
);
assert_eq!(selected.len(), 1);
assert_eq!(selected[0].name, "old_artifact");
assert!(!selected.iter().any(|a| a.name.starts_with("recent_")));
}
#[test]
fn test_combined_selection_timestamp_buffer_edge_case() {
let now = SystemTime::now();
let base_time = now.checked_sub(Duration::from_secs(3600)).unwrap();
let mut artifacts = vec![
create_test_artifact("exactly_at_cutoff", "1111111111111111", 1000, 0),
create_test_artifact("just_before_cutoff", "2222222222222222", 1000, 0),
create_test_artifact("just_after_cutoff", "3333333333333333", 1000, 0),
create_test_artifact("well_before_cutoff", "4444444444444444", 1000, 0),
];
artifacts[0].newest_mtime = base_time; artifacts[1].newest_mtime = base_time.checked_sub(Duration::from_millis(500)).unwrap(); artifacts[2].newest_mtime = base_time.checked_add(Duration::from_millis(500)).unwrap(); artifacts[3].newest_mtime = base_time.checked_sub(Duration::from_secs(6 * 60)).unwrap();
let previous_build_nanos = base_time
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos();
let selected = select_artifacts_for_removal(
&artifacts,
4000,
Some(2000), 30,
Some(previous_build_nanos),
0,
false,
);
assert_eq!(selected.len(), 1);
assert_eq!(selected[0].name, "well_before_cutoff");
}
#[test]
fn test_combined_selection_exceeds_size_for_preservation() {
let now = SystemTime::now();
let recent = now.checked_sub(Duration::from_secs(600)).unwrap(); let old = now.checked_sub(Duration::from_secs(3 * 24 * 3600)).unwrap();
let mut artifacts = vec![
create_test_artifact("old1", "1111111111111111", 2000, 0),
create_test_artifact("old2", "2222222222222222", 2000, 0),
create_test_artifact("recent1", "3333333333333333", 8000, 0), create_test_artifact("recent2", "4444444444444444", 7000, 0), ];
artifacts[0].newest_mtime = old;
artifacts[1].newest_mtime = old;
artifacts[2].newest_mtime = recent;
artifacts[3].newest_mtime = recent;
let previous_build_nanos = recent
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos();
let selected = select_artifacts_for_removal(
&artifacts,
19000,
Some(5000),
30,
Some(previous_build_nanos),
0,
false,
);
assert_eq!(selected.len(), 2);
assert!(selected.iter().all(|a| a.name.starts_with("old")));
}
#[test]
fn test_combined_selection_no_previous_build_timestamp() {
let artifacts = vec![
create_test_artifact("artifact1", "1111111111111111", 5000, 10),
create_test_artifact("artifact2", "2222222222222222", 4000, 5),
create_test_artifact("artifact3", "3333333333333333", 3000, 1),
];
let selected = select_artifacts_for_removal(
&artifacts,
12000,
Some(6000),
30,
None, 0,
false,
);
assert_eq!(selected.len(), 2);
assert_eq!(selected[0].name, "artifact1"); assert_eq!(selected[1].name, "artifact2"); }
#[test]
fn test_combined_selection_all_artifacts_are_recent() {
let now = SystemTime::now();
let recent = now.checked_sub(Duration::from_secs(300)).unwrap();
let mut artifacts = vec![
create_test_artifact("recent1", "1111111111111111", 5000, 0),
create_test_artifact("recent2", "2222222222222222", 5000, 0),
create_test_artifact("recent3", "3333333333333333", 5000, 0),
];
for artifact in &mut artifacts {
artifact.newest_mtime = recent;
}
let previous_build_nanos = recent
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos();
let selected = select_artifacts_for_removal(
&artifacts,
15000,
Some(5000),
30,
Some(previous_build_nanos),
0,
false,
);
assert_eq!(selected.len(), 0);
}
#[test]
fn test_combined_selection_mixed_ages_with_preservation() {
let now = SystemTime::now();
let five_min_ago = now.checked_sub(Duration::from_secs(5 * 60)).unwrap();
let one_hour_ago = now.checked_sub(Duration::from_secs(3600)).unwrap();
let twelve_hours_ago = now.checked_sub(Duration::from_secs(12 * 3600)).unwrap();
let two_days_ago = now.checked_sub(Duration::from_secs(2 * 24 * 3600)).unwrap();
let ten_days_ago = now
.checked_sub(Duration::from_secs(10 * 24 * 3600))
.unwrap();
let mut artifacts = vec![
create_test_artifact("very_old", "1111111111111111", 3000, 0),
create_test_artifact("old", "2222222222222222", 2000, 0),
create_test_artifact("medium_old", "3333333333333333", 2500, 0),
create_test_artifact("recent_preserve", "4444444444444444", 4000, 0),
create_test_artifact("very_recent_preserve", "5555555555555555", 3500, 0),
];
artifacts[0].newest_mtime = ten_days_ago;
artifacts[1].newest_mtime = two_days_ago;
artifacts[2].newest_mtime = twelve_hours_ago;
artifacts[3].newest_mtime = one_hour_ago;
artifacts[4].newest_mtime = five_min_ago;
let previous_build_nanos = one_hour_ago
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos();
let selected = select_artifacts_for_removal(
&artifacts,
15000,
Some(8000),
5,
Some(previous_build_nanos),
0,
false,
);
assert_eq!(selected.len(), 3);
assert!(selected.iter().any(|a| a.name == "very_old"));
assert!(selected.iter().any(|a| a.name == "old"));
assert!(selected.iter().any(|a| a.name == "medium_old"));
assert!(!selected.iter().any(|a| a.name.contains("preserve")));
}
#[test]
fn test_parse_size() {
assert_eq!(parse_size("100").unwrap(), 100);
assert_eq!(parse_size("100B").unwrap(), 100);
assert_eq!(parse_size("1K").unwrap(), 1024);
assert_eq!(parse_size("1KB").unwrap(), 1024);
assert_eq!(parse_size("1KiB").unwrap(), 1024);
assert_eq!(parse_size("2M").unwrap(), 2 * 1024 * 1024);
assert_eq!(parse_size("2MB").unwrap(), 2 * 1024 * 1024);
assert_eq!(parse_size("2MiB").unwrap(), 2 * 1024 * 1024);
assert_eq!(parse_size("3G").unwrap(), 3 * 1024 * 1024 * 1024);
assert_eq!(parse_size("3GB").unwrap(), 3 * 1024 * 1024 * 1024);
assert_eq!(parse_size("3GiB").unwrap(), 3 * 1024 * 1024 * 1024);
assert_eq!(
parse_size("1.5G").unwrap(),
(1.5 * 1024.0 * 1024.0 * 1024.0) as u64
);
assert!(parse_size("").is_err());
assert!(parse_size("abc").is_err());
assert!(parse_size("100X").is_err());
}
#[test]
fn test_format_size() {
assert_eq!(format_size(0), "0 B");
assert_eq!(format_size(100), "100 B");
assert_eq!(format_size(1024), "1.0 KiB");
assert_eq!(format_size(1536), "1.5 KiB");
assert_eq!(format_size(1024 * 1024), "1.0 MiB");
assert_eq!(format_size(1024 * 1024 * 1024), "1.0 GiB");
assert_eq!(format_size(1024_u64.pow(4)), "1.0 TiB");
}
#[test]
fn test_parse_crate_artifact_name_legacy_cases() {
let path = Path::new("libfoo-123456789abcdef0");
let (name, hash) = parse_crate_artifact_name(path).unwrap();
assert_eq!(name, "libfoo");
assert_eq!(hash, "123456789abcdef0");
let path = Path::new("serde-1.0.136-78d1b3f8c7b8e0a2");
let (name, hash) = parse_crate_artifact_name(path).unwrap();
assert_eq!(name, "serde-1.0.136");
assert_eq!(hash, "78d1b3f8c7b8e0a2");
let path = Path::new("foo-bar-baz-0123456789abcdef.d");
let (name, hash) = parse_crate_artifact_name(path).unwrap();
assert_eq!(name, "foo-bar-baz");
assert_eq!(hash, "0123456789abcdef");
assert!(parse_crate_artifact_name(Path::new("foo")).is_none());
assert!(parse_crate_artifact_name(Path::new("foo-123")).is_none());
assert!(parse_crate_artifact_name(Path::new("foo-gggggggggggggggg")).is_none());
}
#[test]
fn test_select_artifacts_with_previous_build_timestamp() {
let now = SystemTime::now();
let five_minutes_ago = now - Duration::from_secs(5 * 60);
let ten_minutes_ago = now - Duration::from_secs(10 * 60);
let one_hour_ago = now - Duration::from_secs(60 * 60);
let two_days_ago = now - Duration::from_secs(2 * 24 * 60 * 60);
let artifacts = vec![
CrateArtifact {
name: "recent-crate".to_string(),
hash: "0000000000000001".to_string(),
artifacts: vec![],
total_size: 1024 * 1024, newest_mtime: five_minutes_ago,
},
CrateArtifact {
name: "previous-build-crate".to_string(),
hash: "0000000000000002".to_string(),
artifacts: vec![],
total_size: 2 * 1024 * 1024, newest_mtime: ten_minutes_ago,
},
CrateArtifact {
name: "old-crate".to_string(),
hash: "0000000000000003".to_string(),
artifacts: vec![],
total_size: 3 * 1024 * 1024, newest_mtime: one_hour_ago,
},
CrateArtifact {
name: "very-old-crate".to_string(),
hash: "0000000000000004".to_string(),
artifacts: vec![],
total_size: 4 * 1024 * 1024, newest_mtime: two_days_ago,
},
];
let previous_build_nanos = ten_minutes_ago
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos();
let to_remove = select_artifacts_for_removal(
&artifacts,
10 * 1024 * 1024, Some(5 * 1024 * 1024), 1, Some(previous_build_nanos),
0, false,
);
assert_eq!(to_remove.len(), 2);
assert!(to_remove.iter().any(|a| a.name == "very-old-crate"));
assert!(to_remove.iter().any(|a| a.name == "old-crate"));
let to_remove_no_preserve = select_artifacts_for_removal(
&artifacts,
10 * 1024 * 1024, Some(5 * 1024 * 1024), 1, None, 0, false,
);
assert!(to_remove_no_preserve.len() >= 2);
assert!(
to_remove_no_preserve
.iter()
.any(|a| a.name == "very-old-crate")
);
}
#[test]
fn test_select_artifacts_skips_stale_previous_timestamp() {
let now = SystemTime::now();
let ten_days_ago = now - Duration::from_secs(10 * 24 * 60 * 60);
let two_days_ago = now - Duration::from_secs(2 * 24 * 60 * 60);
let stale_previous = now - Duration::from_secs(30 * 24 * 60 * 60);
let stale_nanos = stale_previous
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos();
let artifacts = vec![
CrateArtifact {
name: "old-crate".to_string(),
hash: "aaaaaaaaaaaaaaaa".to_string(),
artifacts: vec![],
total_size: 2 * 1024 * 1024,
newest_mtime: ten_days_ago,
},
CrateArtifact {
name: "recent-crate".to_string(),
hash: "bbbbbbbbbbbbbbbb".to_string(),
artifacts: vec![],
total_size: 2 * 1024 * 1024,
newest_mtime: two_days_ago,
},
];
let to_remove = select_artifacts_for_removal(
&artifacts,
4 * 1024 * 1024,
None,
7,
Some(stale_nanos),
0,
false,
);
assert_eq!(to_remove.len(), 1);
assert_eq!(to_remove[0].name, "old-crate");
}
#[test]
fn test_select_artifacts_preserves_recent_previous_timestamp_with_buffer() {
let now = SystemTime::now();
let two_minutes_ago = now - Duration::from_secs(2 * 60);
let eight_days_ago = now - Duration::from_secs(8 * 24 * 60 * 60);
let previous_build_nanos = now
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos();
let artifacts = vec![
CrateArtifact {
name: "recent-build".to_string(),
hash: "cccccccccccccccc".to_string(),
artifacts: vec![],
total_size: 3 * 1024 * 1024,
newest_mtime: two_minutes_ago,
},
CrateArtifact {
name: "older-build".to_string(),
hash: "dddddddddddddddd".to_string(),
artifacts: vec![],
total_size: 3 * 1024 * 1024,
newest_mtime: eight_days_ago,
},
];
let to_remove = select_artifacts_for_removal(
&artifacts,
6 * 1024 * 1024,
Some(1024 * 1024),
7,
Some(previous_build_nanos),
0,
false,
);
assert_eq!(to_remove.len(), 1);
assert_eq!(to_remove[0].name, "older-build");
}
#[test]
fn test_size_cleanup_after_previous_build_expires() {
let now = SystemTime::now();
let fresh = now - Duration::from_secs(60);
let artifacts = vec![
CrateArtifact {
name: "fresh-a".to_string(),
hash: "aaaaaaaaaaaaaaaa".to_string(),
artifacts: vec![],
total_size: 3 * 1024 * 1024,
newest_mtime: fresh,
},
CrateArtifact {
name: "fresh-b".to_string(),
hash: "bbbbbbbbbbbbbbbb".to_string(),
artifacts: vec![],
total_size: 3 * 1024 * 1024,
newest_mtime: fresh,
},
];
let current_size = 6 * 1024 * 1024;
let cap = 4 * 1024 * 1024;
let age_threshold_days = 1;
let previous_build_nanos = now
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos();
let preserved = select_artifacts_for_removal(
&artifacts,
current_size,
Some(cap),
age_threshold_days,
Some(previous_build_nanos),
0,
false,
);
assert!(preserved.is_empty());
let stale_previous = now - Duration::from_secs(2 * 24 * 60 * 60);
let stale_previous_nanos = stale_previous
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos();
let evicted = select_artifacts_for_removal(
&artifacts,
current_size,
Some(cap),
age_threshold_days,
Some(stale_previous_nanos),
0,
false,
);
assert!(!evicted.is_empty());
let freed: u64 = evicted.iter().map(|a| a.total_size).sum();
assert!(freed >= current_size - cap);
}
#[test]
fn test_rejuvenate_stale_artifact_mtimes_enables_preservation() {
use std::fs;
use std::io::Write;
use tempfile::TempDir;
use crate::timestamp::set_file_mtime;
let temp_dir = TempDir::new().unwrap();
let deps_dir = temp_dir.path().join("deps");
fs::create_dir_all(&deps_dir).unwrap();
let now = SystemTime::now();
let one_hour_ago = now.checked_sub(Duration::from_secs(3600)).unwrap();
let one_day_ago = now.checked_sub(Duration::from_secs(24 * 3600)).unwrap();
let forty_days_ago = now
.checked_sub(Duration::from_secs(40 * 24 * 3600))
.unwrap();
let write_artifact = |name: &str, hash: &str, size: u64, mtime: SystemTime| -> CrateArtifact {
let path = deps_dir.join(format!("lib{name}-{hash}.rlib"));
let mut file = fs::File::create(&path).unwrap();
file.write_all(&vec![0u8; size as usize]).unwrap();
set_file_mtime(&path, mtime).unwrap();
CrateArtifact {
name: name.to_string(),
hash: hash.to_string(),
artifacts: vec![artifact_info(path, size, mtime, true)],
total_size: size,
newest_mtime: mtime,
}
};
let mut artifacts = vec![
write_artifact("stale_recent", "2222222222222222", 4000, one_day_ago),
write_artifact("stale_recent2", "3333333333333333", 3000, one_day_ago),
write_artifact("old_artifact", "1111111111111111", 5000, forty_days_ago),
];
let previous_build_nanos = one_hour_ago
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos();
let before = select_artifacts_for_removal(
&artifacts,
14000,
Some(6000),
30,
Some(previous_build_nanos),
0,
true,
);
assert!(
before.iter().any(|a| a.name.starts_with("stale_")),
"stale cache mtimes should be selected for removal before rejuvenation"
);
rejuvenate_stale_artifact_mtimes(
&mut artifacts,
Some(previous_build_nanos),
30,
false,
0,
true,
)
.unwrap();
let after = select_artifacts_for_removal(
&artifacts,
14000,
Some(6000),
30,
Some(previous_build_nanos),
0,
true,
);
assert!(
!after.iter().any(|a| a.name.starts_with("stale_")),
"refreshed mtimes should be preserved as previous-build artifacts"
);
assert_eq!(after.len(), 1);
assert_eq!(after[0].name, "old_artifact");
}
#[test]
fn test_repeated_rejuvenated_gc_does_not_churn_current_artifacts() {
let now = SystemTime::now();
let one_hour_ago = now.checked_sub(Duration::from_secs(3600)).unwrap();
let one_day_ago = now.checked_sub(Duration::from_secs(24 * 3600)).unwrap();
let forty_days_ago = now
.checked_sub(Duration::from_secs(40 * 24 * 3600))
.unwrap();
let mut artifacts = vec![
CrateArtifact {
name: "hot_a".to_string(),
hash: "aaaaaaaaaaaaaaaa".to_string(),
artifacts: vec![artifact_info("hot_a.rlib", 6_000, one_day_ago, true)],
total_size: 6_000,
newest_mtime: one_day_ago,
},
CrateArtifact {
name: "hot_b".to_string(),
hash: "bbbbbbbbbbbbbbbb".to_string(),
artifacts: vec![artifact_info("hot_b.rlib", 5_000, one_day_ago, true)],
total_size: 5_000,
newest_mtime: one_day_ago,
},
CrateArtifact {
name: "cold".to_string(),
hash: "cccccccccccccccc".to_string(),
artifacts: vec![artifact_info("cold.rlib", 4_000, forty_days_ago, true)],
total_size: 4_000,
newest_mtime: forty_days_ago,
},
];
let mut previous_build_nanos = one_hour_ago
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos();
for run in 0..4 {
rejuvenate_stale_artifact_mtimes(
&mut artifacts,
Some(previous_build_nanos),
7,
true,
0,
true,
)
.unwrap();
let current_size: u64 = artifacts.iter().map(|artifact| artifact.total_size).sum();
let selected = select_artifacts_for_removal(
&artifacts,
current_size,
Some(1_000),
7,
Some(previous_build_nanos),
0,
true,
);
let selected_names: Vec<String> = selected
.iter()
.map(|artifact| artifact.name.clone())
.collect();
assert!(
!selected_names.iter().any(|name| name.starts_with("hot_")),
"run {run}: current build artifacts should not churn under a tight cap"
);
if run == 0 {
assert_eq!(selected_names, vec!["cold".to_string()]);
} else {
assert!(
selected_names.is_empty(),
"run {run}: once cold artifacts are gone, repeated GC should be stable"
);
}
artifacts.retain(|artifact| !selected_names.contains(&artifact.name));
previous_build_nanos = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos();
}
}
#[test]
#[cfg(unix)]
fn test_rejuvenate_stale_artifact_mtimes_skips_symlinks() {
use std::fs;
use std::io::Write;
use std::os::unix::fs::symlink;
use tempfile::TempDir;
use crate::timestamp::set_file_mtime;
let temp_dir = TempDir::new().unwrap();
let fingerprint_dir = temp_dir
.path()
.join(".fingerprint")
.join("protobuf-src-4444444444444444");
fs::create_dir_all(&fingerprint_dir).unwrap();
let build_artifact_dir = temp_dir
.path()
.join("build")
.join("protobuf-src-4444444444444444")
.join("out")
.join("build")
.join("src")
.join(".libs");
fs::create_dir_all(&build_artifact_dir).unwrap();
let now = SystemTime::now();
let one_hour_ago = now.checked_sub(Duration::from_secs(3600)).unwrap();
let one_day_ago = now.checked_sub(Duration::from_secs(24 * 3600)).unwrap();
let artifact_path = build_artifact_dir.join("libprotobuf-lite.a");
let mut file = fs::File::create(&artifact_path).unwrap();
file.write_all(&[0u8; 16]).unwrap();
set_file_mtime(&artifact_path, one_day_ago).unwrap();
let link_path = build_artifact_dir.join("libprotobuf-lite.la");
symlink(&artifact_path, &link_path).unwrap();
let stale_filetime = filetime::FileTime::from_system_time(one_day_ago);
filetime::set_symlink_file_times(&link_path, stale_filetime, stale_filetime).unwrap();
let mut artifacts = collect_crate_artifacts(temp_dir.path()).unwrap();
let artifact = artifacts
.iter()
.find(|artifact| artifact.name == "protobuf-src")
.unwrap();
assert!(artifact.artifacts.iter().any(|info| info.path == link_path));
let previous_build_nanos = one_hour_ago
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos();
let touched = rejuvenate_stale_artifact_mtimes(
&mut artifacts,
Some(previous_build_nanos),
30,
false,
0,
true,
)
.unwrap();
assert_eq!(touched, 1);
}