mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
`Stills` is only used by the `#[cfg(unix)]` part of `footage_on_other_computers_is_never_looked_at`. On Windows it was dead code, so `cargo xtask ci` stopped at clippy (`-D warnings`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
156 lines
7.1 KiB
Rust
156 lines
7.1 KiB
Rust
use std::sync::{Arc, Mutex};
|
|
|
|
use effectcraft_project::{Footage, ItemId, ItemKind};
|
|
use serde_json::json;
|
|
|
|
use crate::{Event, Services, Session};
|
|
|
|
/// File access that records every path it is asked about.
|
|
#[derive(Default)]
|
|
struct Recording(Mutex<Vec<String>>);
|
|
|
|
impl Recording {
|
|
fn note(&self, path: &str) {
|
|
self.0.lock().unwrap().push(path.to_string());
|
|
}
|
|
}
|
|
|
|
impl Services for Recording {
|
|
fn read_file(&self, path: &str) -> std::io::Result<Vec<u8>> {
|
|
self.note(path);
|
|
std::fs::read(path)
|
|
}
|
|
fn write_file(&self, path: &str, data: &[u8]) -> std::io::Result<()> {
|
|
std::fs::write(path, data)
|
|
}
|
|
fn exists(&self, path: &str) -> bool {
|
|
self.note(path);
|
|
std::path::Path::new(path).exists()
|
|
}
|
|
fn stamp(&self, path: &str) -> Option<(u64, u128)> {
|
|
self.note(path);
|
|
let meta = std::fs::metadata(path).ok()?;
|
|
Some((meta.len(), meta.modified().ok()?.duration_since(std::time::UNIX_EPOCH).ok()?.as_nanos()))
|
|
}
|
|
}
|
|
|
|
/// Probes every file as a still. Only the Unix part of the test below imports files.
|
|
#[cfg(unix)]
|
|
struct Stills;
|
|
|
|
#[cfg(unix)]
|
|
impl crate::Importer for Stills {
|
|
fn probe(&self, path: &str) -> Result<Footage, String> {
|
|
if !std::path::Path::new(path).is_file() {
|
|
return Err(format!("{path}: no such file"));
|
|
}
|
|
Ok(Footage { path: path.into(), kind: effectcraft_project::FootageKind::Still, width: 64, height: 32, has_video: true, ..Default::default() })
|
|
}
|
|
}
|
|
|
|
fn footage_id(s: &Session, name: &str) -> ItemId {
|
|
s.project.items.values().find(|i| i.name == name).unwrap().id
|
|
}
|
|
|
|
fn footage<'a>(s: &'a Session, name: &str) -> &'a Footage {
|
|
match &s.project.item(footage_id(s, name)).unwrap().kind {
|
|
ItemKind::Footage(f) => f,
|
|
_ => panic!("{name} is not footage"),
|
|
}
|
|
}
|
|
|
|
/// Footage paths to other computers stored in a project are never looked at: not on open, by
|
|
/// the footage check, by the changed-on-disk scan, by Edit Original or by Reveal in Finder. A
|
|
/// relative path is taken from the project's folder, and a file the user imports in this session
|
|
/// is used as given until the project is saved and opened again.
|
|
#[test]
|
|
fn footage_on_other_computers_is_never_looked_at() {
|
|
let root = std::env::temp_dir().join(format!("ec-doc-path-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&root);
|
|
let dir = root.join("job");
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
std::fs::create_dir_all(root.join("media")).unwrap();
|
|
std::fs::write(dir.join("clip.png"), b"x").unwrap();
|
|
std::fs::write(root.join("media").join("far.png"), b"x").unwrap();
|
|
let mut stored = vec![("unc", r"\\192.0.2.1\share\x.png".to_string()), ("url", "file://192.0.2.1/s/x.png".to_string()), ("beside", "clip.png".to_string())];
|
|
// Linux reads `//tmp/…/far.png` as the local file: a sink that skipped the check would find it.
|
|
if cfg!(unix) {
|
|
stored.push(("slashes", format!("/{}", root.join("media").join("far.png").display())));
|
|
}
|
|
let mut s = Session::default();
|
|
for (name, path) in &stored {
|
|
let f = Footage { path: path.clone(), width: 64, height: 48, has_video: true, ..Default::default() };
|
|
Arc::make_mut(&mut s.project).add_item(name, crate::color::Label::None, None, ItemKind::Footage(f));
|
|
}
|
|
let project = dir.join("p.ecproj");
|
|
s.execute("file.saveAs", json!({"path": project.to_string_lossy()})).unwrap();
|
|
|
|
let rec = Arc::new(Recording::default());
|
|
let mut o = Session { services: rec.clone(), ..Default::default() };
|
|
o.execute("file.open", json!({"path": project.to_string_lossy()})).unwrap();
|
|
for (name, _) in &stored {
|
|
assert_eq!(footage(&o, name).missing, *name != "beside", "{name}");
|
|
}
|
|
assert_eq!(std::path::Path::new(&footage(&o, "beside").path), dir.join("clip.png"));
|
|
let r = o.execute("footage.check", json!({"wait": true})).unwrap();
|
|
assert_eq!(r["missing"].as_u64(), Some(stored.len() as u64 - 1), "{r}");
|
|
o.execute("footage.reloadChanged", json!({})).unwrap();
|
|
o.drain_events();
|
|
for (name, _) in &stored {
|
|
o.state.project_selection = vec![footage_id(&o, name)];
|
|
let opened = o.execute("edit.editOriginal", json!({}));
|
|
let revealed = o.execute("file.revealInFinder", json!({}));
|
|
assert_eq!((opened.is_ok(), revealed.is_ok()), (*name == "beside", *name == "beside"), "{name}: {opened:?} {revealed:?}");
|
|
}
|
|
let urls: Vec<String> = o.drain_events().into_iter().filter_map(|e| if let Event::OpenUrl(u) = e { Some(u) } else { None }).collect();
|
|
assert_eq!(urls.len(), 2, "{urls:?}");
|
|
|
|
let seen = rec.0.lock().unwrap().clone();
|
|
assert!(seen.iter().any(|p| std::path::Path::new(p) == dir.join("clip.png")), "{seen:?}");
|
|
// Only files in the temporary folder (the project's folder, where moved footage is looked for).
|
|
let local = root.to_string_lossy().into_owned();
|
|
for p in &seen {
|
|
assert!(p.starts_with(&local), "looked at {p}");
|
|
}
|
|
|
|
// Chosen in this session, the same file is followed as given (as a file on a NAS would be)…
|
|
#[cfg(unix)]
|
|
{
|
|
let far = stored.iter().find(|(n, _)| *n == "slashes").unwrap().1.clone();
|
|
o.importer = Some(Arc::new(Stills));
|
|
o.execute("file.import", json!({"paths": [far.clone()]})).unwrap();
|
|
let imported = |s: &Session| -> Vec<ItemId> {
|
|
s.project.items.values().filter(|i| matches!(&i.kind, ItemKind::Footage(f) if f.path == far)).map(|i| i.id).collect()
|
|
};
|
|
let id = *imported(&o).iter().find(|id| matches!(&o.project.item(**id).unwrap().kind, ItemKind::Footage(f) if !f.missing)).unwrap();
|
|
o.state.project_selection = vec![id];
|
|
o.execute("edit.editOriginal", json!({})).unwrap();
|
|
o.execute("file.revealInFinder", json!({})).unwrap();
|
|
assert!(o.execute("footage.check", json!({"wait": true, "items": [id.0]})).unwrap()["missing"] == 0);
|
|
// …and is a stored path again once the project is saved and opened.
|
|
o.execute("file.save", json!({})).unwrap();
|
|
let mut q = Session::default();
|
|
q.execute("file.open", json!({"path": project.to_string_lossy()})).unwrap();
|
|
let ids = imported(&q);
|
|
assert_eq!(ids.len(), 2);
|
|
for id in ids {
|
|
assert!(matches!(&q.project.item(id).unwrap().kind, ItemKind::Footage(f) if f.missing));
|
|
q.state.project_selection = vec![id];
|
|
assert!(q.execute("edit.editOriginal", json!({})).is_err());
|
|
assert!(q.execute("file.revealInFinder", json!({})).is_err());
|
|
}
|
|
}
|
|
let _ = std::fs::remove_dir_all(&root);
|
|
}
|
|
|
|
#[test]
|
|
fn stored_paths_resolve_against_the_project() {
|
|
let doc = std::env::temp_dir().join("art").join("intro.ecproj");
|
|
let doc = doc.to_str().unwrap();
|
|
assert_eq!(super::resolve("clip.mov", Some(doc)), Ok(std::env::temp_dir().join("art").join("clip.mov")));
|
|
assert_eq!(super::resolve("clip.mov", None), Err(super::NotFollowed::NoProjectFolder));
|
|
assert_eq!(super::resolve(r"\\192.0.2.1\share\x.mov", Some(doc)), Err(super::NotFollowed::Refused(super::Refused::Network)));
|
|
#[cfg(windows)]
|
|
assert!(super::resolve(r"\\server\share\x.mov", Some(r"\\SERVER\share\job\intro.ecproj")).is_ok());
|
|
}
|