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::serialize(&(ent.path, ent.mode, ent.offsets.len()))?;
67 hasher.update(contents.len().to_le_bytes());
68 hasher.update(&contents);
69 }
70 let offsets = hasher.finalize();
71
72 Ok(Self { history, offsets })
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
79pub struct CondaFingerprint {
80 pub conda_meta: CondaMetaFingerprint,
81 pub pack_meta: PackMetaFingerprint,
82}
83
84impl CondaFingerprint {
85 pub async fn from_env(path: &Path) -> Result<Self> {
86 Ok(Self {
87 conda_meta: CondaMetaFingerprint::from_env(path).await?,
88 pack_meta: PackMetaFingerprint::from_env(path).await?,
89 })
90 }
91
92 pub fn mtime_comparator(
96 a: &Self,
97 b: &Self,
98 ) -> Result<Box<dyn Fn(&SystemTime, &SystemTime) -> std::cmp::Ordering + Send + Sync>> {
99 let (a_prefix, a_base) = a.pack_meta.history.first()?;
100 let (b_prefix, b_base) = b.pack_meta.history.first()?;
101 ensure!(a_prefix == b_prefix);
102
103 let slop = Duration::from_mins(5);
106
107 let a_base = UNIX_EPOCH + Duration::from_secs(a_base) + slop;
110 let b_base = UNIX_EPOCH + Duration::from_secs(b_base) + slop;
111
112 let a_window = a
115 .pack_meta
116 .history
117 .prefix_and_last_update_window()?
118 .1
119 .map(|(s, e)| {
120 (
121 UNIX_EPOCH + Duration::from_secs(s),
122 UNIX_EPOCH + Duration::from_secs(e + 1),
123 )
124 });
125 let b_window = b
126 .pack_meta
127 .history
128 .prefix_and_last_update_window()?
129 .1
130 .map(|(s, e)| {
131 (
132 UNIX_EPOCH + Duration::from_secs(s),
133 UNIX_EPOCH + Duration::from_secs(e + 1),
134 )
135 });
136
137 Ok(Box::new(move |a: &SystemTime, b: &SystemTime| {
138 match (
139 *a > a_base && a_window.is_none_or(|(s, e)| *a < s || *a > e),
140 *b > b_base && b_window.is_none_or(|(s, e)| *b < s || *b > e),
141 ) {
142 (true, false) => std::cmp::Ordering::Greater,
143 (false, true) => std::cmp::Ordering::Less,
144 (false, false) => std::cmp::Ordering::Equal,
145 (true, true) => a.cmp(b),
146 }
147 }))
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use std::path::PathBuf;
154 use std::time::SystemTime;
155
156 use anyhow::Result;
157 use rattler_conda_types::package::FileMode;
158 use tempfile::TempDir;
159 use tokio::fs;
160
161 use super::*;
162 use crate::pack_meta_history::HistoryRecord;
163 use crate::pack_meta_history::Offset;
164 use crate::pack_meta_history::OffsetRecord;
165
166 async fn setup_conda_env_with_config(
168 temp_dir: &TempDir,
169 env_name: &str,
170 base_time: SystemTime,
171 prefix: &str,
172 packages: &[(&str, &str, &str)], include_update_window: bool,
174 ) -> Result<PathBuf> {
175 let env_path = temp_dir.path().join(env_name);
176 fs::create_dir_all(&env_path).await?;
177
178 let conda_meta_path = env_path.join("conda-meta");
180 fs::create_dir_all(&conda_meta_path).await?;
181
182 for (name, version, build) in packages {
184 let package_json = format!(
185 r#"{{
186 "name": "{}",
187 "version": "{}",
188 "build": "{}",
189 "build_number": 0,
190 "paths_data": {{
191 "paths": [
192 {{
193 "path": "lib/{}.so",
194 "path_type": "hardlink",
195 "size_in_bytes": 1024,
196 "mode": "binary"
197 }}
198 ]
199 }},
200 "repodata_record": {{
201 "package_record": {{
202 "timestamp": {}
203 }}
204 }}
205 }}"#,
206 name,
207 version,
208 build,
209 name,
210 base_time.duration_since(UNIX_EPOCH)?.as_secs()
211 );
212
213 fs::write(
214 conda_meta_path.join(format!("{}-{}-{}.json", name, version, build)),
215 package_json,
216 )
217 .await?;
218 }
219
220 let pack_meta_path = env_path.join("pack-meta");
222 fs::create_dir_all(&pack_meta_path).await?;
223
224 create_history_file(&pack_meta_path, prefix, base_time, include_update_window).await?;
226
227 create_offsets_file(&pack_meta_path, packages).await?;
229
230 Ok(env_path)
231 }
232
233 async fn create_dummy_conda_env(temp_dir: &TempDir, env_name: &str) -> Result<PathBuf> {
235 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
236 let default_packages = &[
237 ("python", "3.9.0", "h12debd9_1"),
238 ("numpy", "1.21.0", "py39h7a9d4c0_0"),
239 ];
240 setup_conda_env_with_config(
241 temp_dir,
242 env_name,
243 base_time,
244 "/opt/conda",
245 default_packages,
246 false,
247 )
248 .await
249 }
250
251 async fn create_history_file(
253 pack_meta_path: &Path,
254 prefix: &str,
255 base_time: SystemTime,
256 include_update_window: bool,
257 ) -> Result<()> {
258 let base_timestamp = base_time.duration_since(UNIX_EPOCH)?.as_secs();
259 let mut history = History { entries: vec![] };
260
261 history.entries.push(HistoryRecord {
263 timestamp: base_timestamp,
264 prefix: PathBuf::from(prefix),
265 finished: true,
266 });
267
268 if include_update_window {
270 history.entries.push(HistoryRecord {
271 timestamp: base_timestamp + 3600, prefix: PathBuf::from(prefix),
273 finished: false,
274 });
275 history.entries.push(HistoryRecord {
276 timestamp: base_timestamp + 3660, prefix: PathBuf::from(prefix),
278 finished: true,
279 });
280 }
281
282 fs::write(pack_meta_path.join("history.jsonl"), history.to_str()?).await?;
283 Ok(())
284 }
285
286 async fn create_offsets_file(
288 pack_meta_path: &Path,
289 packages: &[(&str, &str, &str)],
290 ) -> Result<()> {
291 let mut offset_entries = Vec::new();
292
293 offset_entries.push(OffsetRecord {
295 path: PathBuf::from("bin/python"),
296 mode: FileMode::Binary,
297 offsets: vec![Offset {
298 start: 0,
299 len: 1024,
300 contents: None,
301 }],
302 });
303
304 for (name, _, _) in packages {
306 offset_entries.push(OffsetRecord {
307 path: PathBuf::from(format!("lib/{}.so", name)),
308 mode: FileMode::Binary,
309 offsets: vec![Offset {
310 start: 0,
311 len: 1024,
312 contents: None,
313 }],
314 });
315 }
316
317 let offsets = Offsets {
318 entries: offset_entries,
319 };
320 fs::write(pack_meta_path.join("offsets.jsonl"), offsets.to_str()?).await?;
321 Ok(())
322 }
323
324 #[tokio::test]
325 async fn test_conda_fingerprint_creation() -> Result<()> {
326 let temp_dir = TempDir::new()?;
327 let env_path = create_dummy_conda_env(&temp_dir, "test_env").await?;
328
329 let fingerprint = CondaFingerprint::from_env(&env_path).await?;
331
332 assert_eq!(fingerprint.pack_meta.history.entries.len(), 1);
334 assert_eq!(
335 fingerprint.pack_meta.history.entries[0].timestamp,
336 1640995200
337 );
338 assert_eq!(
339 fingerprint.pack_meta.history.entries[0].prefix,
340 PathBuf::from("/opt/conda")
341 );
342 assert!(fingerprint.pack_meta.history.entries[0].finished);
343
344 Ok(())
345 }
346
347 #[tokio::test]
348 async fn test_conda_fingerprint_equality() -> Result<()> {
349 let temp_dir = TempDir::new()?;
350
351 let env1_path = create_dummy_conda_env(&temp_dir, "env1").await?;
353 let env2_path = create_dummy_conda_env(&temp_dir, "env2").await?;
354
355 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
356 let default_packages = &[
357 ("python", "3.9.0", "h12debd9_1"),
358 ("numpy", "1.21.0", "py39h7a9d4c0_0"),
359 ];
360 for env_path in [&env1_path, &env2_path] {
361 let pack_meta_path = env_path.join("pack-meta");
362 create_history_file(&pack_meta_path, "/opt/conda", base_time, false).await?;
363 create_offsets_file(&pack_meta_path, default_packages).await?;
364 }
365
366 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
368 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
369
370 assert_eq!(fingerprint1, fingerprint2);
372
373 Ok(())
374 }
375
376 #[tokio::test]
377 async fn test_conda_fingerprint_inequality_different_packages() -> Result<()> {
378 let temp_dir = TempDir::new()?;
379
380 let env1_path = create_dummy_conda_env(&temp_dir, "env1").await?;
382 let env2_path = create_dummy_conda_env(&temp_dir, "env2").await?;
383
384 let conda_meta2_path = env2_path.join("conda-meta");
386 fs::write(
387 conda_meta2_path.join("scipy-1.7.0-py39h7a9d4c0_0.json"),
388 r#"{"name": "scipy", "version": "1.7.0", "build": "py39h7a9d4c0_0"}"#,
389 )
390 .await?;
391
392 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
393 let default_packages = &[
394 ("python", "3.9.0", "h12debd9_1"),
395 ("numpy", "1.21.0", "py39h7a9d4c0_0"),
396 ];
397 for env_path in [&env1_path, &env2_path] {
398 let pack_meta_path = env_path.join("pack-meta");
399 create_history_file(&pack_meta_path, "/opt/conda", base_time, false).await?;
400 create_offsets_file(&pack_meta_path, default_packages).await?;
401 }
402
403 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
405 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
406
407 assert_ne!(fingerprint1, fingerprint2);
409
410 Ok(())
411 }
412
413 #[tokio::test]
414 async fn test_mtime_comparator_basic() -> Result<()> {
415 let temp_dir = TempDir::new()?;
416
417 let env1_path = create_dummy_conda_env(&temp_dir, "env1").await?;
419 let env2_path = create_dummy_conda_env(&temp_dir, "env2").await?;
420
421 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
422 let default_packages = &[
423 ("python", "3.9.0", "h12debd9_1"),
424 ("numpy", "1.21.0", "py39h7a9d4c0_0"),
425 ];
426 for env_path in [&env1_path, &env2_path] {
427 let pack_meta_path = env_path.join("pack-meta");
428 create_history_file(&pack_meta_path, "/opt/conda", base_time, false).await?;
429 create_offsets_file(&pack_meta_path, default_packages).await?;
430 }
431
432 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
434 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
435
436 let comparator = CondaFingerprint::mtime_comparator(&fingerprint1, &fingerprint2)?;
438
439 let base_timestamp = 1640995200;
441 let test_base_time = UNIX_EPOCH + Duration::from_secs(base_timestamp);
442 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);
447
448 assert_eq!(
450 comparator(&new_time, &old_time),
451 std::cmp::Ordering::Greater
452 );
453 assert_eq!(comparator(&old_time, &new_time), std::cmp::Ordering::Less);
454
455 let newer_time = new_time + Duration::from_mins(30);
457 assert_eq!(comparator(&new_time, &newer_time), std::cmp::Ordering::Less);
458 assert_eq!(
459 comparator(&newer_time, &new_time),
460 std::cmp::Ordering::Greater
461 );
462
463 Ok(())
464 }
465
466 #[tokio::test]
467 async fn test_mtime_comparator_with_update_window() -> Result<()> {
468 let temp_dir = TempDir::new()?;
469
470 let env1_path = create_dummy_conda_env(&temp_dir, "env1").await?;
472 let env2_path = create_dummy_conda_env(&temp_dir, "env2").await?;
473
474 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
475 let default_packages = &[
476 ("python", "3.9.0", "h12debd9_1"),
477 ("numpy", "1.21.0", "py39h7a9d4c0_0"),
478 ];
479 for env_path in [&env1_path, &env2_path] {
480 let pack_meta_path = env_path.join("pack-meta");
481 create_history_file(&pack_meta_path, "/opt/conda", base_time, true).await?;
483 create_offsets_file(&pack_meta_path, default_packages).await?;
484 }
485
486 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
488 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
489
490 let comparator = CondaFingerprint::mtime_comparator(&fingerprint1, &fingerprint2)?;
492
493 let base_timestamp = 1640995200;
494 let test_base_time = UNIX_EPOCH + Duration::from_secs(base_timestamp);
495 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);
502 assert_eq!(
503 comparator(&in_window_time, &old_time),
504 std::cmp::Ordering::Equal
505 );
506 assert_eq!(
507 comparator(&old_time, &in_window_time),
508 std::cmp::Ordering::Equal
509 );
510
511 assert_eq!(
513 comparator(&after_window_time, &old_time),
514 std::cmp::Ordering::Greater
515 );
516 assert_eq!(
517 comparator(&old_time, &after_window_time),
518 std::cmp::Ordering::Less
519 );
520
521 Ok(())
522 }
523
524 #[tokio::test]
525 async fn test_mtime_comparator_different_prefixes_fails() -> Result<()> {
526 let temp_dir = TempDir::new()?;
527
528 let env1_path = create_dummy_conda_env(&temp_dir, "env1").await?;
530 let env2_path = create_dummy_conda_env(&temp_dir, "env2").await?;
531
532 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
533 let default_packages = &[
534 ("python", "3.9.0", "h12debd9_1"),
535 ("numpy", "1.21.0", "py39h7a9d4c0_0"),
536 ];
537
538 create_history_file(
540 &env1_path.join("pack-meta"),
541 "/opt/conda1",
542 base_time,
543 false,
544 )
545 .await?;
546 create_history_file(
547 &env2_path.join("pack-meta"),
548 "/opt/conda2",
549 base_time,
550 false,
551 )
552 .await?;
553
554 for env_path in [&env1_path, &env2_path] {
555 create_offsets_file(&env_path.join("pack-meta"), default_packages).await?;
556 }
557
558 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
560 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
561
562 let result = CondaFingerprint::mtime_comparator(&fingerprint1, &fingerprint2);
564 assert!(result.is_err());
565
566 Ok(())
567 }
568
569 #[tokio::test]
570 async fn test_pack_meta_fingerprint_offsets_hashing() -> Result<()> {
571 let temp_dir = TempDir::new()?;
572
573 let env1_path = create_dummy_conda_env(&temp_dir, "env1").await?;
575 let env2_path = create_dummy_conda_env(&temp_dir, "env2").await?;
576
577 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
578 for env_path in [&env1_path, &env2_path] {
579 let pack_meta_path = env_path.join("pack-meta");
580 create_history_file(&pack_meta_path, "/opt/conda", base_time, false).await?;
581 }
582
583 let default_packages = &[
585 ("python", "3.9.0", "h12debd9_1"),
586 ("numpy", "1.21.0", "py39h7a9d4c0_0"),
587 ];
588 for env_path in [&env1_path, &env2_path] {
589 create_offsets_file(&env_path.join("pack-meta"), default_packages).await?;
590 }
591
592 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
594 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
595
596 assert_eq!(fingerprint1.pack_meta, fingerprint2.pack_meta);
598
599 let env3_path = create_dummy_conda_env(&temp_dir, "env3").await?;
601 let pack_meta3_path = env3_path.join("pack-meta");
602 create_history_file(&pack_meta3_path, "/opt/conda", base_time, false).await?;
603
604 let different_offsets = Offsets {
606 entries: vec![OffsetRecord {
607 path: PathBuf::from("bin/python"),
608 mode: FileMode::Binary,
609 offsets: vec![
610 Offset {
611 start: 0,
612 len: 1024,
613 contents: None,
614 },
615 Offset {
616 start: 2048,
617 len: 512,
618 contents: None,
619 }, ],
621 }],
622 };
623 fs::write(
624 pack_meta3_path.join("offsets.jsonl"),
625 different_offsets.to_str()?,
626 )
627 .await?;
628
629 let fingerprint3 = CondaFingerprint::from_env(&env3_path).await?;
630
631 assert_ne!(fingerprint1.pack_meta, fingerprint3.pack_meta);
633
634 Ok(())
635 }
636
637 #[tokio::test]
638 async fn test_mtime_comparator_complex_scenarios() -> Result<()> {
639 let temp_dir = TempDir::new()?;
640 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
641
642 let packages = &[
644 ("python", "3.9.0", "h12debd9_1"),
645 ("numpy", "1.21.0", "py39h7a9d4c0_0"),
646 ("scipy", "1.7.0", "py39h0123456_0"),
647 ];
648
649 let env1_path = setup_conda_env_with_config(
650 &temp_dir,
651 "env1",
652 base_time,
653 "/opt/conda",
654 packages,
655 true, )
657 .await?;
658
659 let env2_path = setup_conda_env_with_config(
660 &temp_dir,
661 "env2",
662 base_time,
663 "/opt/conda",
664 packages,
665 true, )
667 .await?;
668
669 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
671 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
672
673 let comparator = CondaFingerprint::mtime_comparator(&fingerprint1, &fingerprint2)?;
675
676 let base_timestamp = base_time.duration_since(UNIX_EPOCH)?.as_secs();
678 let slop = Duration::from_mins(5);
679 let base_plus_slop = UNIX_EPOCH + Duration::from_secs(base_timestamp) + slop;
680
681 let just_before_slop = base_plus_slop - Duration::from_secs(30);
683 let just_after_slop = base_plus_slop + Duration::from_secs(30);
684
685 assert_eq!(
687 comparator(&just_before_slop, &just_before_slop),
688 std::cmp::Ordering::Equal
689 );
690
691 assert_eq!(
693 comparator(&just_after_slop, &just_before_slop),
694 std::cmp::Ordering::Greater
695 );
696 assert_eq!(
697 comparator(&just_before_slop, &just_after_slop),
698 std::cmp::Ordering::Less
699 );
700
701 Ok(())
702 }
703
704 #[tokio::test]
705 async fn test_mtime_comparator_mixed_update_windows() -> Result<()> {
706 let temp_dir = TempDir::new()?;
707 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
708
709 let packages = &[("python", "3.9.0", "h12debd9_1")];
711
712 let env1_path = setup_conda_env_with_config(
713 &temp_dir,
714 "env1",
715 base_time,
716 "/opt/conda",
717 packages,
718 true, )
720 .await?;
721
722 let env2_path = setup_conda_env_with_config(
723 &temp_dir,
724 "env2",
725 base_time,
726 "/opt/conda",
727 packages,
728 false, )
730 .await?;
731
732 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
734 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
735
736 let comparator = CondaFingerprint::mtime_comparator(&fingerprint1, &fingerprint2)?;
738
739 let base_timestamp = base_time.duration_since(UNIX_EPOCH)?.as_secs();
740 let update_window_time = UNIX_EPOCH + Duration::from_secs(base_timestamp + 3630); let old_time = base_time - Duration::from_hours(1);
742 let new_time = base_time + Duration::from_hours(2);
743
744 assert_eq!(
747 comparator(&update_window_time, &old_time),
748 std::cmp::Ordering::Equal
749 );
750
751 assert_eq!(
754 comparator(&old_time, &update_window_time),
755 std::cmp::Ordering::Less
756 );
757
758 assert_eq!(
760 comparator(&new_time, &old_time),
761 std::cmp::Ordering::Greater
762 );
763 assert_eq!(
764 comparator(&new_time, &update_window_time),
765 std::cmp::Ordering::Greater
766 );
767
768 Ok(())
769 }
770
771 #[tokio::test]
772 async fn test_mtime_comparator_version_differences() -> Result<()> {
773 let temp_dir = TempDir::new()?;
774 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
775
776 let env1_packages = &[
778 ("python", "3.9.0", "h12debd9_1"),
779 ("numpy", "1.21.0", "py39h7a9d4c0_0"),
780 ];
781
782 let env2_packages = &[
783 ("python", "3.9.0", "h12debd9_1"),
784 ("numpy", "1.22.0", "py39h7a9d4c0_0"), ];
786
787 let env1_path = setup_conda_env_with_config(
788 &temp_dir,
789 "env1",
790 base_time,
791 "/opt/conda",
792 env1_packages,
793 false,
794 )
795 .await?;
796
797 let env2_path = setup_conda_env_with_config(
798 &temp_dir,
799 "env2",
800 base_time,
801 "/opt/conda",
802 env2_packages,
803 false,
804 )
805 .await?;
806
807 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
809 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
810
811 assert_ne!(fingerprint1, fingerprint2);
813
814 let comparator = CondaFingerprint::mtime_comparator(&fingerprint1, &fingerprint2)?;
816
817 let old_time = base_time - Duration::from_hours(1);
818 let new_time = base_time + Duration::from_hours(2);
819
820 assert_eq!(comparator(&old_time, &old_time), std::cmp::Ordering::Equal);
822 assert_eq!(
823 comparator(&new_time, &old_time),
824 std::cmp::Ordering::Greater
825 );
826
827 Ok(())
828 }
829
830 #[tokio::test]
831 async fn test_mtime_comparator_empty_environments() -> Result<()> {
832 let temp_dir = TempDir::new()?;
833 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
834
835 let minimal_packages = &[("python", "3.9.0", "h12debd9_1")];
837
838 let env1_path = setup_conda_env_with_config(
839 &temp_dir,
840 "env1",
841 base_time,
842 "/opt/conda",
843 minimal_packages,
844 false,
845 )
846 .await?;
847
848 let env2_path = setup_conda_env_with_config(
849 &temp_dir,
850 "env2",
851 base_time,
852 "/opt/conda",
853 minimal_packages,
854 false,
855 )
856 .await?;
857
858 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
860 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
861
862 let comparator = CondaFingerprint::mtime_comparator(&fingerprint1, &fingerprint2)?;
864
865 let test_time = base_time + Duration::from_mins(30);
867 assert_eq!(
868 comparator(&test_time, &test_time),
869 std::cmp::Ordering::Equal
870 );
871
872 let very_old_time = UNIX_EPOCH + Duration::from_secs(1000);
874 assert_eq!(
875 comparator(&very_old_time, &very_old_time),
876 std::cmp::Ordering::Equal
877 );
878
879 Ok(())
880 }
881
882 #[tokio::test]
883 async fn test_conda_fingerprint_with_large_environments() -> Result<()> {
884 let temp_dir = TempDir::new()?;
885 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
886
887 let large_package_set = &[
889 ("python", "3.9.0", "h12debd9_1"),
890 ("numpy", "1.21.0", "py39h7a9d4c0_0"),
891 ("scipy", "1.7.0", "py39h0123456_0"),
892 ("pandas", "1.3.0", "py39h0abcdef_0"),
893 ("matplotlib", "3.4.0", "py39h0fedcba_0"),
894 ("scikit-learn", "0.24.0", "py39h0987654_0"),
895 ("tensorflow", "2.6.0", "py39h0321654_0"),
896 ("pytorch", "1.9.0", "py39h0456789_0"),
897 ("jupyter", "1.0.0", "py39h0111111_0"),
898 ("ipython", "7.25.0", "py39h0222222_0"),
899 ];
900
901 let env1_path = setup_conda_env_with_config(
902 &temp_dir,
903 "env1",
904 base_time,
905 "/opt/conda",
906 large_package_set,
907 true,
908 )
909 .await?;
910
911 let env2_path = setup_conda_env_with_config(
912 &temp_dir,
913 "env2",
914 base_time,
915 "/opt/conda",
916 large_package_set,
917 true,
918 )
919 .await?;
920
921 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
923 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
924
925 assert_eq!(fingerprint1, fingerprint2);
927
928 let comparator = CondaFingerprint::mtime_comparator(&fingerprint1, &fingerprint2)?;
930
931 let old_time = base_time - Duration::from_hours(1);
932 let new_time = base_time + Duration::from_hours(2);
933
934 assert_eq!(comparator(&old_time, &old_time), std::cmp::Ordering::Equal);
935 assert_eq!(
936 comparator(&new_time, &old_time),
937 std::cmp::Ordering::Greater
938 );
939
940 Ok(())
941 }
942
943 #[tokio::test]
944 async fn test_mtime_comparator_boundary_conditions() -> Result<()> {
945 let temp_dir = TempDir::new()?;
946 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1640995200);
947 let packages = &[("python", "3.9.0", "h12debd9_1")];
948
949 let env1_path = setup_conda_env_with_config(
950 &temp_dir,
951 "env1",
952 base_time,
953 "/opt/conda",
954 packages,
955 true, )
957 .await?;
958
959 let env2_path = setup_conda_env_with_config(
960 &temp_dir,
961 "env2",
962 base_time,
963 "/opt/conda",
964 packages,
965 true, )
967 .await?;
968
969 let fingerprint1 = CondaFingerprint::from_env(&env1_path).await?;
971 let fingerprint2 = CondaFingerprint::from_env(&env2_path).await?;
972
973 let comparator = CondaFingerprint::mtime_comparator(&fingerprint1, &fingerprint2)?;
975
976 let base_timestamp = base_time.duration_since(UNIX_EPOCH)?.as_secs();
977 let slop = Duration::from_mins(5);
978
979 let _exact_base_plus_slop = UNIX_EPOCH + Duration::from_secs(base_timestamp) + slop;
981 let update_window_start = UNIX_EPOCH + Duration::from_secs(base_timestamp + 3600);
982 let update_window_end = UNIX_EPOCH + Duration::from_secs(base_timestamp + 3661); let one_sec_before_window = update_window_start - Duration::from_secs(1);
986 let one_sec_after_window = update_window_end + Duration::from_secs(1);
987 let old_time = base_time - Duration::from_hours(1);
988
989 assert_eq!(
991 comparator(&one_sec_before_window, &old_time),
992 std::cmp::Ordering::Greater
993 );
994
995 assert_eq!(
997 comparator(&one_sec_after_window, &old_time),
998 std::cmp::Ordering::Greater
999 );
1000
1001 assert_eq!(
1003 comparator(&update_window_start, &old_time),
1004 std::cmp::Ordering::Equal
1005 );
1006
1007 let very_far_future = UNIX_EPOCH + Duration::from_secs(u32::MAX as u64);
1009 assert_eq!(
1010 comparator(&very_far_future, &old_time),
1011 std::cmp::Ordering::Greater
1012 );
1013
1014 Ok(())
1015 }
1016}