cargo-hold 1.3.5

cargo-hold: A CI tool to ensure Cargo's incremental compilation is reliable by managing your caches intelligently
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;

use tempfile::TempDir;

use crate::error::HoldError;
use crate::metadata::{
    CapTraceV4, GcMetricsV4, StateMetadataV2, StateMetadataV4, clean_metadata, load_metadata,
    migrate_metadata, save_metadata,
};
use crate::state::{CAP_TRACE_SAMPLE_SOURCE_LEGACY, FileState, METADATA_VERSION, StateMetadata};

#[test]
fn test_save_and_load_metadata() {
    let temp_dir = TempDir::new().unwrap();
    let metadata_path = temp_dir.path().join("test.metadata");

    // Create metadata with some data
    let mut metadata = StateMetadata::new();
    metadata
        .upsert(FileState {
            path: PathBuf::from("test.rs"),
            size: 1234,
            hash: "abcdef".to_string(),
            mtime_nanos: SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_nanos(),
        })
        .unwrap();

    // Save it
    save_metadata(&metadata, &metadata_path).unwrap();
    assert!(metadata_path.exists());

    // Load it back
    let loaded_metadata = load_metadata(&metadata_path).unwrap();
    assert_eq!(loaded_metadata.len(), 1);
    assert!(loaded_metadata.contains(&PathBuf::from("test.rs")).unwrap());
}

#[test]
fn test_load_nonexistent_metadata() {
    let temp_dir = TempDir::new().unwrap();
    let metadata_path = temp_dir.path().join("nonexistent.metadata");

    let metadata = load_metadata(&metadata_path).unwrap();
    assert!(metadata.is_empty());
}

#[test]
fn test_clean_metadata() {
    let temp_dir = TempDir::new().unwrap();
    let metadata_path = temp_dir.path().join("test.metadata");

    // Create a metadata file
    let metadata = StateMetadata::new();
    save_metadata(&metadata, &metadata_path).unwrap();
    assert!(metadata_path.exists());

    // Clean it
    clean_metadata(&metadata_path).unwrap();
    assert!(!metadata_path.exists());

    // Cleaning non-existent file should not error
    clean_metadata(&metadata_path).unwrap();
}

#[test]
fn test_atomic_save() {
    let temp_dir = TempDir::new().unwrap();
    let metadata_path = temp_dir.path().join("test.metadata");

    let metadata = StateMetadata::new();
    save_metadata(&metadata, &metadata_path).unwrap();

    // Temporary file should not exist
    let temp_path = metadata_path.with_extension("tmp");
    assert!(!temp_path.exists());
    assert!(metadata_path.exists());
}

#[test]
fn test_metadata_version() {
    let temp_dir = TempDir::new().unwrap();
    let metadata_path = temp_dir.path().join("test.metadata");

    // Create and save metadata
    let mut metadata = StateMetadata::new();
    metadata
        .upsert(FileState {
            path: PathBuf::from("test.rs"),
            size: 100,
            hash: "hash".to_string(),
            mtime_nanos: 123456789,
        })
        .unwrap();
    save_metadata(&metadata, &metadata_path).unwrap();

    // Load and check version
    let loaded_metadata = load_metadata(&metadata_path).unwrap();
    assert_eq!(loaded_metadata.version, METADATA_VERSION);
    assert_eq!(loaded_metadata.len(), 1);
}

#[test]
fn test_metadata_migration_v2_to_v3_adds_gc_metrics() {
    let temp_dir = TempDir::new().unwrap();
    let metadata_path = temp_dir.path().join("test.metadata");

    // Simulate v2 metadata on disk (without gc_metrics field).
    let v2 = StateMetadataV2 {
        version: 2,
        files: HashMap::new(),
        last_gc_mtime_nanos: None,
    };
    let bytes = rkyv::to_bytes::<rkyv::rancor::BoxedError>(&v2).unwrap();
    std::fs::write(&metadata_path, bytes).unwrap();

    let loaded = load_metadata(&metadata_path).unwrap();
    assert_eq!(loaded.version, METADATA_VERSION);
    assert_eq!(loaded.gc_metrics.runs, 0);
}

#[test]
fn test_metadata_migration_v4_to_v5_seeds_healthy_sizing_finals() {
    let temp_dir = TempDir::new().unwrap();
    let metadata_path = temp_dir.path().join("test.metadata");

    let v4 = StateMetadataV4 {
        version: 4,
        files: HashMap::new(),
        last_gc_mtime_nanos: None,
        gc_metrics: GcMetricsV4 {
            runs: 3,
            seed_initial_size: Some(100),
            recent_initial_sizes: vec![100, 150, 250],
            recent_bytes_freed: vec![0, 25, 25],
            last_suggested_cap: Some(150),
            recent_final_sizes: vec![100, 125, 225],
            last_cap_trace: Some(CapTraceV4 {
                baseline: 125,
                growth_budget: 25,
                observed_growth_pct: 10,
                clamp_reason: "within-window".to_string(),
            }),
        },
    };
    let bytes = rkyv::to_bytes::<rkyv::rancor::BoxedError>(&v4).unwrap();
    std::fs::write(&metadata_path, bytes).unwrap();

    let loaded = load_metadata(&metadata_path).unwrap();
    assert_eq!(loaded.version, METADATA_VERSION);
    assert_eq!(loaded.gc_metrics.recent_final_sizes, vec![100, 125, 225]);
    assert_eq!(loaded.gc_metrics.recent_sizing_final_sizes, vec![100, 125]);
    assert!(loaded.gc_metrics.recent_cap_overage_bytes.is_empty());
    let trace = loaded.gc_metrics.last_cap_trace.unwrap();
    assert_eq!(trace.sample_source, CAP_TRACE_SAMPLE_SOURCE_LEGACY);
    assert_eq!(trace.sample_count, 0);
}

#[test]
fn test_metadata_migration_v1_to_v3() {
    let temp_dir = TempDir::new().unwrap();
    let metadata_path = temp_dir.path().join("test.metadata");

    // Create v1 metadata manually (simulate old version)
    let mut metadata = StateMetadata::new();
    metadata.version = 1; // Force to v1
    metadata
        .upsert(FileState {
            path: PathBuf::from("test.rs"),
            size: 100,
            hash: "hash".to_string(),
            mtime_nanos: 123456789,
        })
        .unwrap();

    // Save with v1
    save_metadata(&metadata, &metadata_path).unwrap();

    // Load should migrate to latest
    let loaded_metadata = load_metadata(&metadata_path).unwrap();
    assert_eq!(loaded_metadata.version, METADATA_VERSION);
    assert_eq!(loaded_metadata.len(), 1);
    assert!(loaded_metadata.last_gc_mtime_nanos.is_none()); // Should be None after migration
    assert_eq!(loaded_metadata.gc_metrics.runs, 0);
}

#[test]
fn test_last_gc_mtime_nanos_preservation() {
    let temp_dir = TempDir::new().unwrap();
    let metadata_path = temp_dir.path().join("test.metadata");

    // Create metadata with some files
    let mut metadata = StateMetadata::new();
    metadata
        .upsert(FileState {
            path: PathBuf::from("file1.rs"),
            size: 100,
            hash: "hash1".to_string(),
            mtime_nanos: 1000000000,
        })
        .unwrap();
    metadata
        .upsert(FileState {
            path: PathBuf::from("file2.rs"),
            size: 200,
            hash: "hash2".to_string(),
            mtime_nanos: 2000000000,
        })
        .unwrap();

    assert_eq!(metadata.max_mtime_nanos(), Some(2000000000));

    // Save and load again
    save_metadata(&metadata, &metadata_path).unwrap();
    let loaded = load_metadata(&metadata_path).unwrap();

    // Create new metadata and set last_gc_mtime_nanos
    let mut new_metadata = StateMetadata::new();
    new_metadata.last_gc_mtime_nanos = loaded.max_mtime_nanos();
    new_metadata
        .upsert(FileState {
            path: PathBuf::from("file3.rs"),
            size: 300,
            hash: "hash3".to_string(),
            mtime_nanos: 3000000000,
        })
        .unwrap();

    save_metadata(&new_metadata, &metadata_path).unwrap();

    // Load and verify
    let final_metadata = load_metadata(&metadata_path).unwrap();
    assert_eq!(final_metadata.last_gc_mtime_nanos, Some(2000000000));
    assert_eq!(final_metadata.max_mtime_nanos(), Some(3000000000));
}

#[test]
fn test_format_incompatibility_recovery() {
    let temp_dir = TempDir::new().unwrap();
    let metadata_path = temp_dir.path().join("test.metadata");

    // Create a file with invalid/corrupted data that will cause deserialization to
    // fail This simulates the scenario where old metadata format can't be
    // read
    let invalid_data = b"this is not valid rkyv data and should cause deserialization to fail";
    fs::write(&metadata_path, invalid_data).unwrap();

    // Verify the file exists and contains our invalid data
    assert!(metadata_path.exists());
    assert_eq!(fs::read(&metadata_path).unwrap(), invalid_data);

    // Attempt to load metadata - should recover gracefully and return fresh
    // metadata
    let result = load_metadata(&metadata_path);
    assert!(result.is_ok());

    let metadata = result.unwrap();

    // Should be fresh metadata
    assert_eq!(metadata.version, METADATA_VERSION);
    assert_eq!(metadata.len(), 0);
    assert!(metadata.last_gc_mtime_nanos.is_none());

    // The invalid file should have been removed during recovery
    assert!(!metadata_path.exists());
}

#[test]
fn test_format_incompatibility_with_subsequent_save() {
    let temp_dir = TempDir::new().unwrap();
    let metadata_path = temp_dir.path().join("test.metadata");

    // Create invalid data to simulate old format
    let invalid_data = b"invalid rkyv data representing old format";
    fs::write(&metadata_path, invalid_data).unwrap();

    // Load should recover gracefully
    let mut metadata = load_metadata(&metadata_path).unwrap();
    assert_eq!(metadata.version, METADATA_VERSION);
    assert_eq!(metadata.len(), 0);

    // Add some data and save
    metadata
        .upsert(FileState {
            path: PathBuf::from("test.rs"),
            size: 100,
            hash: "testhash".to_string(),
            mtime_nanos: 1234567890,
        })
        .unwrap();

    save_metadata(&metadata, &metadata_path).unwrap();

    // Should be able to load the new format without issues
    let reloaded = load_metadata(&metadata_path).unwrap();
    assert_eq!(reloaded.version, METADATA_VERSION);
    assert_eq!(reloaded.len(), 1);
    assert!(reloaded.get(Path::new("test.rs")).unwrap().is_some());
}

#[test]
fn test_version_migration_logic() {
    // Test the migration function directly since we can't easily create true v1
    // files with the current structure (rkyv serialization includes the
    // struct definition)

    // Create metadata that simulates v1 structure
    let mut v1_metadata = StateMetadata::new();
    v1_metadata.version = 1; // Manually set to v1
    v1_metadata
        .upsert(FileState {
            path: PathBuf::from("legacy.rs"),
            size: 200,
            hash: "legacyhash".to_string(),
            mtime_nanos: 9876543210,
        })
        .unwrap();

    // The v1 structure didn't have last_gc_mtime_nanos, so it should be None
    assert!(v1_metadata.last_gc_mtime_nanos.is_none());
    assert_eq!(v1_metadata.version, 1);

    // Test migration function directly
    let migrated = migrate_metadata(v1_metadata).unwrap();

    // Verify migration occurred
    assert_eq!(migrated.version, METADATA_VERSION); // Should be current now
    assert_eq!(migrated.len(), 1);
    assert!(migrated.get(Path::new("legacy.rs")).unwrap().is_some());
    assert!(migrated.last_gc_mtime_nanos.is_none()); // migration preserves None
    assert_eq!(migrated.gc_metrics.runs, 0);
}

#[test]
fn test_future_version_handling() {
    let temp_dir = TempDir::new().unwrap();
    let metadata_path = temp_dir.path().join("test.metadata");

    // Create metadata with a future version
    let mut future_metadata = StateMetadata::new();
    future_metadata.version = METADATA_VERSION + 1; // Future version

    save_metadata(&future_metadata, &metadata_path).unwrap();

    // Should return a ConfigError for future versions
    let result = load_metadata(&metadata_path);
    assert!(result.is_err());

    match result.unwrap_err() {
        HoldError::ConfigError(message) => {
            assert!(message.contains("newer than supported"));
            assert!(message.contains(&(METADATA_VERSION + 1).to_string()));
        }
        other => panic!("Expected ConfigError, got: {other:?}"),
    }
}

#[test]
fn test_real_world_incompatible_format_scenario() {
    let temp_dir = TempDir::new().unwrap();
    let metadata_path = temp_dir.path().join("test.metadata");

    // Simulate the exact error the user encountered by creating data that
    // would cause "hash table length must be strictly less than its capacity"
    // This mimics old format data that can't be deserialized
    let problematic_data = [
        0x72, 0x6b, 0x79, 0x76, // rkyv magic
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // corrupted length
        0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // corrupted capacity
    ];

    fs::write(&metadata_path, problematic_data).unwrap();

    // Load should detect incompatibility and recover gracefully
    let metadata = load_metadata(&metadata_path).unwrap();

    // Verify recovery worked
    assert_eq!(metadata.version, METADATA_VERSION);
    assert_eq!(metadata.len(), 0);
    assert!(metadata.last_gc_mtime_nanos.is_none());

    // Old file should be gone
    assert!(!metadata_path.exists());

    // Should be able to use the recovered metadata normally
    let mut recovered = metadata;
    recovered
        .upsert(FileState {
            path: PathBuf::from("recovered.rs"),
            size: 42,
            hash: "recovered".to_string(),
            mtime_nanos: 12345,
        })
        .unwrap();

    // Save should work normally after recovery
    save_metadata(&recovered, &metadata_path).unwrap();

    // And subsequent loads should work fine
    let final_metadata = load_metadata(&metadata_path).unwrap();
    assert_eq!(final_metadata.len(), 1);
    assert!(
        final_metadata
            .get(Path::new("recovered.rs"))
            .unwrap()
            .is_some()
    );
}