Files
oak-editor/crates/oak-task/tests/import_test.rs
T
Mike-Solar e04ce03058 test(oak-task, oak-storage): task managers, codec bridge, storage
Task/manager lifecycles, precache and render boundaries, OTIO/FCPXML
round trips, and the write-through/library contract tests.
2026-09-22 20:54:04 +08:00

242 lines
7.2 KiB
Rust

// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Boundary tests for `ProjectImportTask` (media import): valid and
//! invalid files, directories (recursive folder creation), duplicate
//! paths, the produced undo command, and cancellation.
//!
//! Media comes from `oak_codec::testmedia`, so the tests run headless.
use oak_node::folder::FolderBehavior;
use oak_node::id::NodeId;
use oak_node::project::Project;
use oak_task::nodeops::{self, ProjectRef};
use oak_task::project::import::ProjectImportTask;
use oak_task::task::{Task, TaskBehavior};
fn work_dir(tag: &str) -> std::path::PathBuf {
let path = std::env::temp_dir().join(format!(
"oaktask_import_{tag}_{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&path);
std::fs::create_dir_all(&path).expect("work dir");
path
}
fn write_clip(dir: &std::path::Path, name: &str) -> std::path::PathBuf {
let path = dir.join(name);
oak_codec::testmedia::write_test_clip(&path, 64, 64, 10, 10).expect("test clip generation");
path
}
/// A fresh project with its initialized root folder.
fn project() -> (ProjectRef, NodeId) {
let project = Project::new();
let root = {
let mut p = project.lock().unwrap();
p.initialize().expect("root folder");
p.root
};
(project, root)
}
fn run_import(
task: &mut ProjectImportTask,
driver: &mut Task,
) -> oak_task::error::Result<()> {
task.run(driver)
}
fn folder_children(project: &ProjectRef, folder: NodeId) -> Vec<NodeId> {
let guard = project.lock().unwrap_or_else(|e| e.into_inner());
guard
.graph
.get(folder)
.and_then(|e| e.behavior.as_any())
.and_then(|a| a.downcast_ref::<FolderBehavior>())
.map(|f| f.children.clone())
.unwrap_or_default()
}
#[test]
fn import_mixes_valid_and_invalid_files() {
let dir = work_dir("mixed");
let media = write_clip(&dir, "clip.mp4");
let missing = dir.join("missing.mp4");
let (project, root) = project();
let mut task = ProjectImportTask::new(
Task::new("Import", None),
(project.clone(), root),
project.clone(),
vec![
media.to_string_lossy().into_owned(),
missing.to_string_lossy().into_owned(),
],
None,
2,
);
let mut driver = Task::new("Import", None);
assert!(run_import(&mut task, &mut driver).is_ok());
assert_eq!(task.file_count(), 2);
assert_eq!(task.get_file_count(), 1, "one footage imported");
assert_eq!(task.get_invalid_file_count(), 1, "one file failed");
assert!(task.has_invalid_files());
assert!(!task.get_imported_footage(0).is_err());
assert!(task.get_imported_footage(1).is_err(), "out of range");
let (project_ref, footage) = task.get_imported_footage(0).expect("footage");
assert_eq!(nodeops::footage_filename(&project_ref, footage), media.to_string_lossy());
assert!(nodeops::footage_is_valid(&project_ref, footage));
// The task's command adds the footage to the destination folder; undo
// removes it again.
let mut cmd = task.take_command().expect("command produced");
assert!(folder_children(&project, root).is_empty());
cmd.redo_now();
assert_eq!(folder_children(&project, root), vec![footage]);
cmd.undo_now();
assert!(folder_children(&project, root).is_empty());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn import_duplicate_paths_create_one_footage_each() {
let dir = work_dir("dupes");
let media = write_clip(&dir, "clip.mp4");
let path = media.to_string_lossy().into_owned();
let (project, root) = project();
let mut task = ProjectImportTask::new(
Task::new("Import", None),
(project.clone(), root),
project.clone(),
vec![path.clone(), path],
None,
2,
);
let mut driver = Task::new("Import", None);
assert!(run_import(&mut task, &mut driver).is_ok());
assert_eq!(task.get_file_count(), 2, "each entry imports");
assert_eq!(task.get_invalid_file_count(), 0);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn import_directory_creates_a_labeled_folder_recursively() {
let dir = work_dir("directory");
let sub = dir.join("Shots");
std::fs::create_dir_all(&sub).expect("subdir");
write_clip(&sub, "a.mp4");
write_clip(&sub, "b.mp4");
let (project, root) = project();
let mut task = ProjectImportTask::new(
Task::new("Import", None),
(project.clone(), root),
project.clone(),
vec![sub.to_string_lossy().into_owned()],
None,
2,
);
let mut driver = Task::new("Import", None);
assert!(run_import(&mut task, &mut driver).is_ok());
assert_eq!(task.get_file_count(), 2, "directory children imported");
assert_eq!(task.get_invalid_file_count(), 0);
// The directory became a labeled sub-folder holding both footages.
let mut cmd = task.take_command().expect("command produced");
cmd.redo_now();
let children = folder_children(&project, root);
assert_eq!(children.len(), 1, "one folder added");
let sub_folder = children[0];
assert_eq!(nodeops::node_label(&project, sub_folder), "Shots");
assert_eq!(folder_children(&project, sub_folder).len(), 2);
cmd.undo_now();
assert!(folder_children(&project, root).is_empty());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn import_empty_directory_imports_nothing() {
let dir = work_dir("empty");
let empty = dir.join("Nothing");
std::fs::create_dir_all(&empty).expect("empty dir");
let (project, root) = project();
let mut task = ProjectImportTask::new(
Task::new("Import", None),
(project.clone(), root),
project.clone(),
vec![empty.to_string_lossy().into_owned()],
None,
0,
);
let mut driver = Task::new("Import", None);
assert!(run_import(&mut task, &mut driver).is_ok());
assert_eq!(task.get_file_count(), 0);
assert_eq!(task.get_invalid_file_count(), 0);
assert!(task.take_command().is_ok(), "an (empty) command is still produced");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn import_cancel_reports_cancelled_and_yields_no_command() {
let dir = work_dir("cancel");
let media = write_clip(&dir, "clip.mp4");
let (project, root) = project();
let mut task = ProjectImportTask::new(
Task::new("Import", None),
(project.clone(), root),
project.clone(),
vec![media.to_string_lossy().into_owned()],
None,
1,
);
let mut driver = Task::new("Import", None);
driver.cancel();
let result = run_import(&mut task, &mut driver);
assert!(
matches!(result, Err(oak_task::error::Error::Cancelled)),
"a cancelled import must fail with Cancelled: {result:?}"
);
assert!(task.take_command().is_err(), "no command after cancellation");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn take_command_before_a_run_reports_state() {
let (project, root) = project();
let mut task = ProjectImportTask::new(
Task::new("Import", None),
(project.clone(), root),
project.clone(),
Vec::new(),
None,
0,
);
assert!(task.take_command().is_err());
}