use super::format_elapsed; use crate::docker::build_dir::{build_checksum, parse_versions_env, serialize_versions_env}; use std::collections::HashMap; use std::fs; use std::path::PathBuf; use std::time::Duration; #[test] fn registry_refs_version_and_latest() { use crate::docker::push::registry_refs; let (v, l) = registry_refs("ghcr.io/kilork", "glm53-jovian-r35:local", "ead6e95a333d").unwrap(); assert_eq!(v, "ghcr.io/kilork/glm53-jovian-r35:ead6e95a333d"); assert_eq!(l, "ghcr.io/kilork/glm53-jovian-r35:latest"); // slashed image name → last segment; base slash-trimmed let (v, _) = registry_refs("ghcr.io/kilork/", "foo/bar:1.2", "abc123").unwrap(); assert_eq!(v, "ghcr.io/kilork/bar:abc123"); // empty base/version → None assert_eq!(registry_refs("", "img:tag", "abc"), None); assert_eq!(registry_refs("ghcr.io/k", "img:tag", ""), None); } fn temp_test_dir(name: &str) -> PathBuf { let p = std::env::temp_dir().join(format!("sm121-build-test-{}-{}", std::process::id(), name)); fs::remove_dir_all(&p).ok(); fs::create_dir_all(&p).unwrap(); p } #[test] fn format_elapsed_seconds_and_minutes() { assert_eq!(format_elapsed(Duration::from_secs(0)), "0s"); assert_eq!(format_elapsed(Duration::from_secs(30)), "30s"); assert_eq!(format_elapsed(Duration::from_secs(59)), "59s"); assert_eq!(format_elapsed(Duration::from_secs(60)), "1m 00s"); assert_eq!(format_elapsed(Duration::from_secs(90)), "1m 30s"); assert_eq!(format_elapsed(Duration::from_secs(125)), "2m 05s"); } #[test] fn parse_versions_env_skips_comments_and_blanks() { let raw = "# comment\n\nKEY=val\nOTHER=a=b\n SPACED = x \nnoeq\n"; let pins = parse_versions_env(raw); assert_eq!(pins.get("KEY"), Some(&"val".to_string())); assert_eq!(pins.get("OTHER"), Some(&"a=b".to_string())); assert_eq!(pins.get("SPACED"), Some(&"x".to_string())); assert!(!pins.contains_key("noeq")); assert_eq!(pins.len(), 3); } #[test] fn parse_versions_env_quotes_are_normalized() { // quoted / unquoted / single-quoted forms of the same value must // resolve identically (stable checksums across edits); inner quotes // and non-matching outer quotes stay verbatim. let raw = "A=\"x:tag@sha256:abc\"\nB='y'\nC=z\nD=\"m@n\"'\"op\"'\nE=\"half\nF='weird\"\n"; let pins = parse_versions_env(raw); assert_eq!(pins.get("A"), Some(&"x:tag@sha256:abc".to_string())); assert_eq!(pins.get("B"), Some(&"y".to_string())); assert_eq!(pins.get("C"), Some(&"z".to_string())); assert_eq!(pins.get("D"), Some(&"\"m@n\"'\"op\"'".to_string())); // mismatched outer quotes stay assert_eq!(pins.get("E"), Some(&"\"half".to_string())); assert_eq!(pins.get("F"), Some(&"'weird\"".to_string())); } #[test] fn serialize_versions_env_sorted_one_per_line() { let mut pins = std::collections::BTreeMap::new(); pins.insert("B".into(), "2".into()); pins.insert("A".into(), "1".into()); let s = serialize_versions_env(&pins); assert_eq!( s, "# Merged versions.env \u{2014} generated by sm121 (profile + recipe [build.versions] applied)\nA=1\nB=2\n" ); } #[test] fn build_checksum_no_profile_no_overrides_returns_none_and_dir_checksum() { let dir = temp_test_dir("no_overrides"); fs::write(dir.join("versions.env"), "A=1\nB=2\n").unwrap(); let (merged, ck) = build_checksum(&dir, None, &HashMap::new()).unwrap(); assert!(merged.is_none()); let (_, ck2) = build_checksum(&dir, None, &HashMap::new()).unwrap(); assert_eq!(ck, ck2); fs::remove_dir_all(&dir).ok(); } #[test] fn build_checksum_overrides_change_checksum_and_produce_merged() { let dir = temp_test_dir("overrides"); fs::write(dir.join("versions.env"), "A=1\nB=2\n").unwrap(); let no_over = build_checksum(&dir, None, &HashMap::new()).unwrap().1; let mut over = HashMap::new(); over.insert("A".to_string(), "99".to_string()); over.insert("C".to_string(), "3".to_string()); let (merged, with_over) = build_checksum(&dir, None, &over).unwrap(); assert_ne!(no_over, with_over); let m = merged.unwrap(); assert!(m.contains("A=99")); assert!(m.contains("C=3")); assert!(m.contains("B=2")); assert!(!m.contains("\nA=1\n")); over.insert("A".to_string(), "100".to_string()); let (_, ck_changed) = build_checksum(&dir, None, &over).unwrap(); assert_ne!(with_over, ck_changed); fs::remove_dir_all(&dir).ok(); } #[test] fn build_checksum_profile_merges_over_base_and_under_overrides() { let dir = temp_test_dir("profile"); fs::write(dir.join("versions.env"), "A=1\nB=2\nC=3\n").unwrap(); fs::create_dir_all(dir.join("profiles")).unwrap(); fs::write( dir.join("profiles").join("gen.env"), "# profile pins (comment line ignored)\nA=10\nC=30\n", ) .unwrap(); // profile alone let (merged, with_profile) = build_checksum(&dir, Some("gen"), &HashMap::new()).unwrap(); let m = merged.unwrap(); assert!(m.contains("A=10"), "profile wins over base: {m}"); assert!(m.contains("C=30"), "{m}"); assert!(m.contains("B=2"), "base key untouched: {m}"); let base = build_checksum(&dir, None, &HashMap::new()).unwrap().1; assert_ne!(base, with_profile, "profile changes the checksum"); // recipe override still wins over the profile let mut over = HashMap::new(); over.insert("A".to_string(), "99".to_string()); let (merged, with_both) = build_checksum(&dir, Some("gen"), &over).unwrap(); let m = merged.unwrap(); assert!(m.contains("A=99"), "recipe wins over profile: {m}"); assert!(m.contains("C=30")); assert_ne!(with_profile, with_both); // a profile edit changes the checksum (the file is in the build dir, // so the dir checksum moves too) fs::write(dir.join("profiles").join("gen.env"), "A=10\nC=31\n").unwrap(); let (_, edited) = build_checksum(&dir, Some("gen"), &HashMap::new()).unwrap(); assert_ne!(with_profile, edited); fs::remove_dir_all(&dir).ok(); } #[test] fn build_checksum_missing_profile_lists_available() { let dir = temp_test_dir("profile_missing"); fs::write(dir.join("versions.env"), "A=1\n").unwrap(); fs::create_dir_all(dir.join("profiles")).unwrap(); fs::write(dir.join("profiles").join("gen.env"), "A=10\n").unwrap(); let err = build_checksum(&dir, Some("nope"), &HashMap::new()) .unwrap_err() .to_string(); assert!(err.contains("build profile 'nope' not found"), "{err}"); assert!( err.contains("gen.env"), "error lists available profiles: {err}" ); fs::remove_dir_all(&dir).ok(); }