1use std::path::Path;
10use std::time::Duration;
11use std::time::SystemTime;
12use std::time::UNIX_EPOCH;
13
14use anyhow::Result;
15use anyhow::ensure;
16use digest::Digest;
17use digest::Output;
18use serde::Deserialize;
19use serde::Serialize;
20use sha2::Sha256;
21use tokio::fs;
22
23use crate::hash_utils;
24use crate::pack_meta_history::History;
25use crate::pack_meta_history::Offsets;
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub struct CondaMetaFingerprint {
30 hash: Output<Sha256>,
34}
35
36impl CondaMetaFingerprint {
37 async fn from_env(path: &Path) -> Result<Self> {
38 let mut hasher = Sha256::new();
39 hash_utils::hash_directory_tree(&path.join("conda-meta"), &mut hasher).await?;
40 Ok(Self {
41 hash: hasher.finalize(),
42 })
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
48pub struct PackMetaFingerprint {
49 offsets: Output<Sha256>,
50 pub history: History,
51}
52
53impl PackMetaFingerprint {
54 async fn from_env(path: &Path) -> Result<Self> {
55 let pack_meta = path.join("pack-meta");
56
57 let contents = fs::read_to_string(pack_meta.join("history.jsonl")).await?;
59 let history = History::from_contents(&contents)?;
60
61 let mut hasher = Sha256::new();
63 let contents = fs::read_to_string(pack_meta.join("offsets.jsonl")).await?;
64 let offsets = Offsets::from_contents(&contents)?;
65 for ent in offsets.entries {
66 let contents = bincode::serde::encode_to_vec(
67 &(ent.path, ent.mode, ent.offsets.len()),
68 bincode::config::legacy(),
69 )?;
70 hasher.update(contents.len().to_le_bytes());
71 hasher.update(&contents);
72 }
73 let offsets = hasher.finalize();
74
75 Ok(Self { history, offsets })
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
82pub struct CondaFingerprint {
83 pub conda_meta: CondaMetaFingerprint,
84 pub pack_meta: PackMetaFingerprint,
85}
86
87impl CondaFingerprint {
88 pub async fn from_env(path: &Path) -> Result<Self> {
89 Ok(Self {
90 conda_meta: CondaMetaFingerprint::from_env(path).await?,
91 pack_meta: PackMetaFingerprint::from_env(path).await?,
92 })
93 }
94
95 pub fn mtime_comparator(
99 a: &Self,
100 b: &Self,
101 ) -> Result<Box<dyn Fn(&SystemTime, &SystemTime) -> std::cmp::Ordering + Send + Sync>> {
102 let (a_prefix, a_base) = a.pack_meta.history.first()?;
103 let (b_prefix, b_base) = b.pack_meta.history.first()?;
104 ensure!(a_prefix == b_prefix);
105
106 let slop = Duration::from_mins(5);
109
110 let a_base = UNIX_EPOCH + Duration::from_secs(a_base) + slop;
113 let b_base = UNIX_EPOCH + Duration::from_secs(b_base) + slop;
114
115 let a_window = a
118 .pack_meta
119 .history
120 .prefix_and_last_update_window()?
121 .1
122 .map(|(s, e)| {
123 (
124 UNIX_EPOCH + Duration::from_secs(s),
125 UNIX_EPOCH + Duration::from_secs(e + 1),
126 )
127 });
128 let b_window = b
129 .pack_meta
130 .history
131 .prefix_and_last_update_window()?
132 .1
133 .map(|(s, e)| {
134 (
135 UNIX_EPOCH + Duration::from_secs(s),
136 UNIX_EPOCH + Duration::from_secs(e + 1),
137 )
138 });
139
140 Ok(Box::new(move |a: &SystemTime, b: &SystemTime| {
141 match (
142 *a > a_base && a_window.is_none_or(|(s, e)| *a < s || *a > e),
143 *b > b_base && b_window.is_none_or(|(s, e)| *b < s || *b > e),
144 ) {
145 (true, false) => std::cmp::Ordering::Greater,
146 (false, true) => std::cmp::Ordering::Less,
147 (false, false) => std::cmp::Ordering::Equal,
148 (true, true) => a.cmp(b),
149 }
150 }))
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use std::path::PathBuf;
157 use std::time::SystemTime;
158
159 use anyhow::Result;
160 use rattler_conda_types::package::FileMode;
161 use tempfile::TempDir;
162 use tokio::fs;
163
164 use super::*;
165 use crate::pack_meta_history::HistoryRecord;
166 use crate::pack_meta_history::Offset;
167 use crate::pack_meta_history::OffsetRecord;
168
169 async fn setup_conda_env_with_config(
171 temp_dir: &TempDir,
172 env_name: &str,
173 base_time: SystemTime,
174 prefix: &str,
175 packages: &[(&str, &str, &str)], include_update_window: bool,
177 ) -> Result<PathBuf> {
178 let env_path = temp_dir.path().join(env_name);
179 fs::create_dir_all(&env_path).await?;
180
181 let conda_meta_path = env_path.join("conda-meta");
183 fs::create_dir_all(&conda_meta_path).await?;
184
185 for (name, version, build) in packages {
187 let package_json = format!(
188 r#"{{
189 "name": "{}",
190 "version": "{}",
191 "build": "{}",
192 "build_number": 0,
193 "paths_data": {{
194 "paths": [
195 {{
196 "path": "lib/{}.so",
197 "path_type": "hardlink",
198 "size_in_bytes": 1024,
199 "mode": "binary"
200 }}
201 ]
202 }},
203 "repodata_record": {{
204 "package_record": {{
205 "timestamp": {}
206 }}
207 }}
208 }}"#,
209 name,
210 version,
211 build,
212 name,
213 base_time.duration_since(UNIX_EPOCH)?.as_secs()
214 );
215
216 fs::write(
217 conda_meta_path.join(format!("{}-{}-{}.json", name, version, build)),
218 package_json,
219 )
220 .await?;
221 }
222
223 let pack_meta_path = env_path.join("pack-meta");
225 fs::create_dir_all(&pack_meta_path).await?;
226
227 create_history_file(&pack_meta_path, prefix, base_time, include_update_window).await?;
229
230 create_offsets_file(&pack_meta_path, packages).await?;
232
233 Ok(env_path)
234 }
235
236 async fn create_dummy_conda_env(temp_dir: &TempDir, env_name: &str) -> Result<PathBuf> {
238 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
239 let default_packages = &[
240 ("python", "3.9.0", "h12debd9_1"),
241 ("numpy", "1.21.0", "py39h7a9d4c0_0"),
242 ];
243 setup_conda_env_with_config(
244 temp_dir,
245 env_name,
246 base_time,
247 "/opt/conda",
248 default_packages,
249 false,
250 )
251 .await
252 }
253
254 async fn create_history_file(
256 pack_meta_path: &Path,
257 prefix: &str,
258 base_time: SystemTime,
259 include_update_window: bool,
260 ) -> Result<()> {
261 let base_timestamp = base_time.duration_since(UNIX_EPOCH)?.as_secs();
262 let mut history = History { entries: vec![] };
263
264 history.entries.push(HistoryRecord {
266 timestamp: base_timestamp,
267 prefix: PathBuf::from(prefix),
268 finished: true,
269 });
270
271 if include_update_window {
273 history.entries.push(HistoryRecord {
274 timestamp: base_timestamp + 3600, prefix: PathBuf::from(prefix),
276 finished: false,
277 });
278 history.entries.push(HistoryRecord {
279 timestamp: base_timestamp + 3660, prefix: PathBuf::from(prefix),
281 finished: true,
282 });
283 }
284
285 fs::write(pack_meta_path.join("history.jsonl"), history.to_str()?).await?;
286 Ok(())
287 }
288
289 async fn create_offsets_file(
291 pack_meta_path: &Path,
292 packages: &[(&str, &str, &str)],
293 ) -> Result<()> {
294 let mut offset_entries = Vec::new();
295
296 offset_entries.push(OffsetRecord {
298 path: PathBuf::from("bin/python"),
299 mode: FileMode::Binary,
300 offsets: vec![Offset {
301 start: 0,
302 len: 1024,
303 contents: None,
304 }],
305 });
306
307 for (name, _, _) in packages {
309 offset_entries.push(OffsetRecord {
310 path: PathBuf::from(format!("lib/{}.so", name)),
311 mode: FileMode::Binary,
312 offsets: vec![Offset {
313 start: 0,
314 len: 1024,
315 contents: None,
316 }],
317 });
318 }
319
320 let offsets = Offsets {
321 entries: offset_entries,
322 };
323 fs::write(pack_meta_path.join("offsets.jsonl"), offsets.to_str()?).await?;
324 Ok(())
325 }
326
327 #[tokio::test]
328 async fn test_conda_fingerprint_creation() -> Result<()> {
329 let temp_dir = TempDir::new()?;
330 let env_path = create_dummy_conda_env(&temp_dir, "test_env").await?;
331
332 let fingerprint = CondaFingerprint::from_env(&env_path).await?;
334
335 assert_eq!(fingerprint.pack_meta.history.entries.len(), 1);
337 assert_eq!(
338 fingerprint.pack_meta.history.entries[0].timestamp,
339 1640995200
340 );
341 assert_eq!(
342 fingerprint.pack_meta.history.entries[0].prefix,
343 PathBuf::from("/opt/conda")
344 );
345 assert!(fingerprint.pack_meta.history.entries[0].finished);
346
347 Ok(())
348 }
349
350 #[tokio::test]
351 async fn test_conda_fingerprint_equality() -> Result<()> {
352 let temp_dir = TempDir::new()?;
353
354 let env1_path = create_dummy_conda_env(&temp_dir, "env1").await?;
356 let env2_path = create_dummy_conda_env(&temp_dir, "env2").await?;
357
358 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
359 let default_packages = &[
360 ("python", "3.9.0", "h12debd9_1"),
361 ("numpy", "1.21.0", "py39h7a9d4c0_0"),
362 ];
363 for env_path in [&env1_path, &env2_path] {
364 let pack_meta_path = env_path.join("pack-meta");
365 create_history_file(&pack_meta_path, "/opt/conda", base_time, false).await?;
366 create_offsets_file(&pack_meta_path, default_packages).await?;
367 }
368
369 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
371 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
372
373 assert_eq!(fingerprint1, fingerprint2);
375
376 Ok(())
377 }
378
379 #[tokio::test]
380 async fn test_conda_fingerprint_inequality_different_packages() -> Result<()> {
381 let temp_dir = TempDir::new()?;
382
383 let env1_path = create_dummy_conda_env(&temp_dir, "env1").await?;
385 let env2_path = create_dummy_conda_env(&temp_dir, "env2").await?;
386
387 let conda_meta2_path = env2_path.join("conda-meta");
389 fs::write(
390 conda_meta2_path.join("scipy-1.7.0-py39h7a9d4c0_0.json"),
391 r#"{"name": "scipy", "version": "1.7.0", "build": "py39h7a9d4c0_0"}"#,
392 )
393 .await?;
394
395 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
396 let default_packages = &[
397 ("python", "3.9.0", "h12debd9_1"),
398 ("numpy", "1.21.0", "py39h7a9d4c0_0"),
399 ];
400 for env_path in [&env1_path, &env2_path] {
401 let pack_meta_path = env_path.join("pack-meta");
402 create_history_file(&pack_meta_path, "/opt/conda", base_time, false).await?;
403 create_offsets_file(&pack_meta_path, default_packages).await?;
404 }
405
406 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
408 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
409
410 assert_ne!(fingerprint1, fingerprint2);
412
413 Ok(())
414 }
415
416 #[tokio::test]
417 async fn test_mtime_comparator_basic() -> Result<()> {
418 let temp_dir = TempDir::new()?;
419
420 let env1_path = create_dummy_conda_env(&temp_dir, "env1").await?;
422 let env2_path = create_dummy_conda_env(&temp_dir, "env2").await?;
423
424 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
425 let default_packages = &[
426 ("python", "3.9.0", "h12debd9_1"),
427 ("numpy", "1.21.0", "py39h7a9d4c0_0"),
428 ];
429 for env_path in [&env1_path, &env2_path] {
430 let pack_meta_path = env_path.join("pack-meta");
431 create_history_file(&pack_meta_path, "/opt/conda", base_time, false).await?;
432 create_offsets_file(&pack_meta_path, default_packages).await?;
433 }
434
435 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
437 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
438
439 let comparator = CondaFingerprint::mtime_comparator(&fingerprint1, &fingerprint2)?;
441
442 let base_timestamp = 1640995200;
444 let test_base_time = UNIX_EPOCH + Duration::from_secs(base_timestamp);
445 let old_time = test_base_time - Duration::from_hours(1); let new_time = test_base_time + Duration::from_hours(2); assert_eq!(comparator(&old_time, &old_time), std::cmp::Ordering::Equal);
450
451 assert_eq!(
453 comparator(&new_time, &old_time),
454 std::cmp::Ordering::Greater
455 );
456 assert_eq!(comparator(&old_time, &new_time), std::cmp::Ordering::Less);
457
458 let newer_time = new_time + Duration::from_mins(30);
460 assert_eq!(comparator(&new_time, &newer_time), std::cmp::Ordering::Less);
461 assert_eq!(
462 comparator(&newer_time, &new_time),
463 std::cmp::Ordering::Greater
464 );
465
466 Ok(())
467 }
468
469 #[tokio::test]
470 async fn test_mtime_comparator_with_update_window() -> Result<()> {
471 let temp_dir = TempDir::new()?;
472
473 let env1_path = create_dummy_conda_env(&temp_dir, "env1").await?;
475 let env2_path = create_dummy_conda_env(&temp_dir, "env2").await?;
476
477 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
478 let default_packages = &[
479 ("python", "3.9.0", "h12debd9_1"),
480 ("numpy", "1.21.0", "py39h7a9d4c0_0"),
481 ];
482 for env_path in [&env1_path, &env2_path] {
483 let pack_meta_path = env_path.join("pack-meta");
484 create_history_file(&pack_meta_path, "/opt/conda", base_time, true).await?;
486 create_offsets_file(&pack_meta_path, default_packages).await?;
487 }
488
489 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
491 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
492
493 let comparator = CondaFingerprint::mtime_comparator(&fingerprint1, &fingerprint2)?;
495
496 let base_timestamp = 1640995200;
497 let test_base_time = UNIX_EPOCH + Duration::from_secs(base_timestamp);
498 let update_start = test_base_time + Duration::from_hours(1); let update_end = test_base_time + Duration::from_mins(61); let in_window_time = update_start + Duration::from_secs(30); let after_window_time = update_end + Duration::from_hours(1); let old_time = base_time - Duration::from_hours(1);
505 assert_eq!(
506 comparator(&in_window_time, &old_time),
507 std::cmp::Ordering::Equal
508 );
509 assert_eq!(
510 comparator(&old_time, &in_window_time),
511 std::cmp::Ordering::Equal
512 );
513
514 assert_eq!(
516 comparator(&after_window_time, &old_time),
517 std::cmp::Ordering::Greater
518 );
519 assert_eq!(
520 comparator(&old_time, &after_window_time),
521 std::cmp::Ordering::Less
522 );
523
524 Ok(())
525 }
526
527 #[tokio::test]
528 async fn test_mtime_comparator_different_prefixes_fails() -> Result<()> {
529 let temp_dir = TempDir::new()?;
530
531 let env1_path = create_dummy_conda_env(&temp_dir, "env1").await?;
533 let env2_path = create_dummy_conda_env(&temp_dir, "env2").await?;
534
535 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
536 let default_packages = &[
537 ("python", "3.9.0", "h12debd9_1"),
538 ("numpy", "1.21.0", "py39h7a9d4c0_0"),
539 ];
540
541 create_history_file(
543 &env1_path.join("pack-meta"),
544 "/opt/conda1",
545 base_time,
546 false,
547 )
548 .await?;
549 create_history_file(
550 &env2_path.join("pack-meta"),
551 "/opt/conda2",
552 base_time,
553 false,
554 )
555 .await?;
556
557 for env_path in [&env1_path, &env2_path] {
558 create_offsets_file(&env_path.join("pack-meta"), default_packages).await?;
559 }
560
561 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
563 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
564
565 let result = CondaFingerprint::mtime_comparator(&fingerprint1, &fingerprint2);
567 assert!(result.is_err());
568
569 Ok(())
570 }
571
572 #[tokio::test]
573 async fn test_pack_meta_fingerprint_offsets_hashing() -> Result<()> {
574 let temp_dir = TempDir::new()?;
575
576 let env1_path = create_dummy_conda_env(&temp_dir, "env1").await?;
578 let env2_path = create_dummy_conda_env(&temp_dir, "env2").await?;
579
580 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
581 for env_path in [&env1_path, &env2_path] {
582 let pack_meta_path = env_path.join("pack-meta");
583 create_history_file(&pack_meta_path, "/opt/conda", base_time, false).await?;
584 }
585
586 let default_packages = &[
588 ("python", "3.9.0", "h12debd9_1"),
589 ("numpy", "1.21.0", "py39h7a9d4c0_0"),
590 ];
591 for env_path in [&env1_path, &env2_path] {
592 create_offsets_file(&env_path.join("pack-meta"), default_packages).await?;
593 }
594
595 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
597 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
598
599 assert_eq!(fingerprint1.pack_meta, fingerprint2.pack_meta);
601
602 let env3_path = create_dummy_conda_env(&temp_dir, "env3").await?;
604 let pack_meta3_path = env3_path.join("pack-meta");
605 create_history_file(&pack_meta3_path, "/opt/conda", base_time, false).await?;
606
607 let different_offsets = Offsets {
609 entries: vec![OffsetRecord {
610 path: PathBuf::from("bin/python"),
611 mode: FileMode::Binary,
612 offsets: vec![
613 Offset {
614 start: 0,
615 len: 1024,
616 contents: None,
617 },
618 Offset {
619 start: 2048,
620 len: 512,
621 contents: None,
622 }, ],
624 }],
625 };
626 fs::write(
627 pack_meta3_path.join("offsets.jsonl"),
628 different_offsets.to_str()?,
629 )
630 .await?;
631
632 let fingerprint3 = CondaFingerprint::from_env(&env3_path).await?;
633
634 assert_ne!(fingerprint1.pack_meta, fingerprint3.pack_meta);
636
637 Ok(())
638 }
639
640 #[tokio::test]
641 async fn test_mtime_comparator_complex_scenarios() -> Result<()> {
642 let temp_dir = TempDir::new()?;
643 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
644
645 let packages = &[
647 ("python", "3.9.0", "h12debd9_1"),
648 ("numpy", "1.21.0", "py39h7a9d4c0_0"),
649 ("scipy", "1.7.0", "py39h0123456_0"),
650 ];
651
652 let env1_path = setup_conda_env_with_config(
653 &temp_dir,
654 "env1",
655 base_time,
656 "/opt/conda",
657 packages,
658 true, )
660 .await?;
661
662 let env2_path = setup_conda_env_with_config(
663 &temp_dir,
664 "env2",
665 base_time,
666 "/opt/conda",
667 packages,
668 true, )
670 .await?;
671
672 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
674 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
675
676 let comparator = CondaFingerprint::mtime_comparator(&fingerprint1, &fingerprint2)?;
678
679 let base_timestamp = base_time.duration_since(UNIX_EPOCH)?.as_secs();
681 let slop = Duration::from_mins(5);
682 let base_plus_slop = UNIX_EPOCH + Duration::from_secs(base_timestamp) + slop;
683
684 let just_before_slop = base_plus_slop - Duration::from_secs(30);
686 let just_after_slop = base_plus_slop + Duration::from_secs(30);
687
688 assert_eq!(
690 comparator(&just_before_slop, &just_before_slop),
691 std::cmp::Ordering::Equal
692 );
693
694 assert_eq!(
696 comparator(&just_after_slop, &just_before_slop),
697 std::cmp::Ordering::Greater
698 );
699 assert_eq!(
700 comparator(&just_before_slop, &just_after_slop),
701 std::cmp::Ordering::Less
702 );
703
704 Ok(())
705 }
706
707 #[tokio::test]
708 async fn test_mtime_comparator_mixed_update_windows() -> Result<()> {
709 let temp_dir = TempDir::new()?;
710 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
711
712 let packages = &[("python", "3.9.0", "h12debd9_1")];
714
715 let env1_path = setup_conda_env_with_config(
716 &temp_dir,
717 "env1",
718 base_time,
719 "/opt/conda",
720 packages,
721 true, )
723 .await?;
724
725 let env2_path = setup_conda_env_with_config(
726 &temp_dir,
727 "env2",
728 base_time,
729 "/opt/conda",
730 packages,
731 false, )
733 .await?;
734
735 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
737 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
738
739 let comparator = CondaFingerprint::mtime_comparator(&fingerprint1, &fingerprint2)?;
741
742 let base_timestamp = base_time.duration_since(UNIX_EPOCH)?.as_secs();
743 let update_window_time = UNIX_EPOCH + Duration::from_secs(base_timestamp + 3630); let old_time = base_time - Duration::from_hours(1);
745 let new_time = base_time + Duration::from_hours(2);
746
747 assert_eq!(
750 comparator(&update_window_time, &old_time),
751 std::cmp::Ordering::Equal
752 );
753
754 assert_eq!(
757 comparator(&old_time, &update_window_time),
758 std::cmp::Ordering::Less
759 );
760
761 assert_eq!(
763 comparator(&new_time, &old_time),
764 std::cmp::Ordering::Greater
765 );
766 assert_eq!(
767 comparator(&new_time, &update_window_time),
768 std::cmp::Ordering::Greater
769 );
770
771 Ok(())
772 }
773
774 #[tokio::test]
775 async fn test_mtime_comparator_version_differences() -> Result<()> {
776 let temp_dir = TempDir::new()?;
777 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
778
779 let env1_packages = &[
781 ("python", "3.9.0", "h12debd9_1"),
782 ("numpy", "1.21.0", "py39h7a9d4c0_0"),
783 ];
784
785 let env2_packages = &[
786 ("python", "3.9.0", "h12debd9_1"),
787 ("numpy", "1.22.0", "py39h7a9d4c0_0"), ];
789
790 let env1_path = setup_conda_env_with_config(
791 &temp_dir,
792 "env1",
793 base_time,
794 "/opt/conda",
795 env1_packages,
796 false,
797 )
798 .await?;
799
800 let env2_path = setup_conda_env_with_config(
801 &temp_dir,
802 "env2",
803 base_time,
804 "/opt/conda",
805 env2_packages,
806 false,
807 )
808 .await?;
809
810 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
812 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
813
814 assert_ne!(fingerprint1, fingerprint2);
816
817 let comparator = CondaFingerprint::mtime_comparator(&fingerprint1, &fingerprint2)?;
819
820 let old_time = base_time - Duration::from_hours(1);
821 let new_time = base_time + Duration::from_hours(2);
822
823 assert_eq!(comparator(&old_time, &old_time), std::cmp::Ordering::Equal);
825 assert_eq!(
826 comparator(&new_time, &old_time),
827 std::cmp::Ordering::Greater
828 );
829
830 Ok(())
831 }
832
833 #[tokio::test]
834 async fn test_mtime_comparator_empty_environments() -> Result<()> {
835 let temp_dir = TempDir::new()?;
836 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
837
838 let minimal_packages = &[("python", "3.9.0", "h12debd9_1")];
840
841 let env1_path = setup_conda_env_with_config(
842 &temp_dir,
843 "env1",
844 base_time,
845 "/opt/conda",
846 minimal_packages,
847 false,
848 )
849 .await?;
850
851 let env2_path = setup_conda_env_with_config(
852 &temp_dir,
853 "env2",
854 base_time,
855 "/opt/conda",
856 minimal_packages,
857 false,
858 )
859 .await?;
860
861 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
863 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
864
865 let comparator = CondaFingerprint::mtime_comparator(&fingerprint1, &fingerprint2)?;
867
868 let test_time = base_time + Duration::from_mins(30);
870 assert_eq!(
871 comparator(&test_time, &test_time),
872 std::cmp::Ordering::Equal
873 );
874
875 let very_old_time = UNIX_EPOCH + Duration::from_secs(1000);
877 assert_eq!(
878 comparator(&very_old_time, &very_old_time),
879 std::cmp::Ordering::Equal
880 );
881
882 Ok(())
883 }
884
885 #[tokio::test]
886 async fn test_conda_fingerprint_with_large_environments() -> Result<()> {
887 let temp_dir = TempDir::new()?;
888 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
889
890 let large_package_set = &[
892 ("python", "3.9.0", "h12debd9_1"),
893 ("numpy", "1.21.0", "py39h7a9d4c0_0"),
894 ("scipy", "1.7.0", "py39h0123456_0"),
895 ("pandas", "1.3.0", "py39h0abcdef_0"),
896 ("matplotlib", "3.4.0", "py39h0fedcba_0"),
897 ("scikit-learn", "0.24.0", "py39h0987654_0"),
898 ("tensorflow", "2.6.0", "py39h0321654_0"),
899 ("pytorch", "1.9.0", "py39h0456789_0"),
900 ("jupyter", "1.0.0", "py39h0111111_0"),
901 ("ipython", "7.25.0", "py39h0222222_0"),
902 ];
903
904 let env1_path = setup_conda_env_with_config(
905 &temp_dir,
906 "env1",
907 base_time,
908 "/opt/conda",
909 large_package_set,
910 true,
911 )
912 .await?;
913
914 let env2_path = setup_conda_env_with_config(
915 &temp_dir,
916 "env2",
917 base_time,
918 "/opt/conda",
919 large_package_set,
920 true,
921 )
922 .await?;
923
924 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
926 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
927
928 assert_eq!(fingerprint1, fingerprint2);
930
931 let comparator = CondaFingerprint::mtime_comparator(&fingerprint1, &fingerprint2)?;
933
934 let old_time = base_time - Duration::from_hours(1);
935 let new_time = base_time + Duration::from_hours(2);
936
937 assert_eq!(comparator(&old_time, &old_time), std::cmp::Ordering::Equal);
938 assert_eq!(
939 comparator(&new_time, &old_time),
940 std::cmp::Ordering::Greater
941 );
942
943 Ok(())
944 }
945
946 #[tokio::test]
947 async fn test_mtime_comparator_boundary_conditions() -> Result<()> {
948 let temp_dir = TempDir::new()?;
949 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
950 let packages = &[("python", "3.9.0", "h12debd9_1")];
951
952 let env1_path = setup_conda_env_with_config(
953 &temp_dir,
954 "env1",
955 base_time,
956 "/opt/conda",
957 packages,
958 true, )
960 .await?;
961
962 let env2_path = setup_conda_env_with_config(
963 &temp_dir,
964 "env2",
965 base_time,
966 "/opt/conda",
967 packages,
968 true, )
970 .await?;
971
972 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
974 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
975
976 let comparator = CondaFingerprint::mtime_comparator(&fingerprint1, &fingerprint2)?;
978
979 let base_timestamp = base_time.duration_since(UNIX_EPOCH)?.as_secs();
980 let slop = Duration::from_mins(5);
981
982 let _exact_base_plus_slop = UNIX_EPOCH + Duration::from_secs(base_timestamp) + slop;
984 let update_window_start = UNIX_EPOCH + Duration::from_secs(base_timestamp + 3600);
985 let update_window_end = UNIX_EPOCH + Duration::from_secs(base_timestamp + 3661); let one_sec_before_window = update_window_start - Duration::from_secs(1);
989 let one_sec_after_window = update_window_end + Duration::from_secs(1);
990 let old_time = base_time - Duration::from_hours(1);
991
992 assert_eq!(
994 comparator(&one_sec_before_window, &old_time),
995 std::cmp::Ordering::Greater
996 );
997
998 assert_eq!(
1000 comparator(&one_sec_after_window, &old_time),
1001 std::cmp::Ordering::Greater
1002 );
1003
1004 assert_eq!(
1006 comparator(&update_window_start, &old_time),
1007 std::cmp::Ordering::Equal
1008 );
1009
1010 let very_far_future = UNIX_EPOCH + Duration::from_secs(u32::MAX as u64);
1012 assert_eq!(
1013 comparator(&very_far_future, &old_time),
1014 std::cmp::Ordering::Greater
1015 );
1016
1017 Ok(())
1018 }
1019}