diff --git a/crates/effects/src/ocio/tests.rs b/crates/effects/src/ocio/tests.rs index fe93dd25a..e17fd782a 100644 --- a/crates/effects/src/ocio/tests.rs +++ b/crates/effects/src/ocio/tests.rs @@ -396,3 +396,43 @@ fn color_stabilizer_samples_the_reference_at_the_points_reference_positions() { assert!((o(x) - (a + b * v(x))).abs() < 1e-3, "x {x}"); } } + +/// Writes `text` to `dir/name` with modification time `secs` (so an edit within the file system's +/// time resolution still moves the stamp), returning the path. +fn write_dated(dir: &std::path::Path, name: &str, text: &str, secs: u64) -> String { + let path = dir.join(name); + std::fs::write(&path, text).unwrap(); + let f = std::fs::File::options().write(true).open(&path).unwrap(); + f.set_modified(std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs)).unwrap(); + path.to_string_lossy().into_owned() +} + +/// #3538: OCIO File Transform read a file once per process; an edited, created, repaired or +/// deleted file was never followed. +#[test] +fn an_ocio_transform_file_is_read_again_once_it_changes() { + const BLACK: &str = "LUT_1D_SIZE 2\n0 0 0\n0 0 0\n"; + const WHITE: &str = "LUT_1D_SIZE 2\n1 1 1\n1 1 1\n"; + let dir = std::env::temp_dir().join(format!("ec-fx-3538-file-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let at = |src: &str| load_file(src).map(|x| x.apply([0.5; 3], 1, false, "")); + let edited = write_dated(dir.as_path(), "edited.cube", BLACK, 1_000); + assert_eq!(at(&edited), Some([0.0; 3])); + write_dated(dir.as_path(), "edited.cube", WHITE, 2_000); + assert_eq!(at(&edited), Some([1.0; 3]), "an edited file, same size"); + let created = dir.as_path().join("created.cube").to_string_lossy().into_owned(); + assert_eq!(at(&created), None); + write_dated(dir.as_path(), "created.cube", WHITE, 1_000); + assert_eq!(at(&created), Some([1.0; 3]), "a file created after a failed load"); + let repaired = write_dated(dir.as_path(), "repaired.cube", "not a lut", 1_000); + assert_eq!(at(&repaired), None); + write_dated(dir.as_path(), "repaired.cube", WHITE, 2_000); + assert_eq!(at(&repaired), Some([1.0; 3]), "a repaired file"); + std::fs::remove_file(&repaired).unwrap(); + assert_eq!(at(&repaired), None, "a deleted file"); + // Inline text is its own content: the same text is the same transform. + let inline = load_file(BLACK).unwrap(); + assert!(Arc::ptr_eq(&inline, &load_file(BLACK).unwrap())); + std::fs::remove_dir_all(&dir).unwrap(); +} diff --git a/crates/effects/src/ocio_config/tests.rs b/crates/effects/src/ocio_config/tests.rs index b616e6263..c36e467ea 100644 --- a/crates/effects/src/ocio_config/tests.rs +++ b/crates/effects/src/ocio_config/tests.rs @@ -410,3 +410,45 @@ fn builtin_styles() { fn rgb_white_xyz() -> [f64; 3] { [D65[0] / D65[1], 1.0, (1.0 - D65[0] - D65[1]) / D65[1]] } + +/// #3538: a custom `.ocio` config, and the LUTs it reads, were read once per process. +#[test] +fn an_ocio_config_and_its_luts_are_read_again_once_they_change() { + const BLACK: &str = "LUT_1D_SIZE 2\n0 0 0\n0 0 0\n"; + const WHITE: &str = "LUT_1D_SIZE 2\n1 1 1\n1 1 1\n"; + let dir = std::env::temp_dir().join(format!("ec-fx-3538-config-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::create_dir_all(dir.as_path().join("luts")).unwrap(); + let write = |name: &str, text: &str, secs: u64| { + let path = dir.as_path().join(name); + std::fs::write(&path, text).unwrap(); + let f = std::fs::File::options().write(true).open(&path).unwrap(); + f.set_modified(std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs)).unwrap(); + path.to_string_lossy().into_owned() + }; + let config = |space: &str| { + format!( + "ocio_profile_version: 2\nsearch_path: luts\nroles:\n scene_linear: lin\ncolorspaces:\n - !\n name: lin\n - !\n name: {space}\n to_reference: ! {{src: grade.cube, interpolation: linear}}\n - !\n name: later\n to_reference: ! {{src: later.cube, interpolation: linear}}\n" + ) + }; + let src = dir.as_path().join("config.ocio").to_string_lossy().into_owned(); + assert!(load_config(&src).is_err(), "no config yet"); + write("config.ocio", &config("graded"), 1_000); + write("luts/grade.cube", BLACK, 1_000); + let to_lin = |space: &str| { + let c = load_config(&src).unwrap(); + c.convert(c.space(space).unwrap(), c.space("lin").unwrap(), [0.5; 3]) + }; + assert_eq!(to_lin("graded"), [0.0; 3], "a config created after a failed load"); + write("luts/grade.cube", WHITE, 2_000); + assert_eq!(to_lin("graded"), [1.0; 3], "a LUT the config reads, edited"); + assert_eq!(to_lin("later"), [0.5; 3], "a LUT not there yet passes through"); + write("luts/later.cube", BLACK, 1_000); + assert_eq!(to_lin("later"), [0.0; 3], "a LUT the config reads, created"); + write("config.ocio", &config("regraded"), 2_000); + assert!(load_config(&src).unwrap().space("regraded").is_some(), "an edited config"); + std::fs::remove_file(&src).unwrap(); + assert!(load_config(&src).is_err(), "a deleted config"); + std::fs::remove_dir_all(&dir).unwrap(); +}