mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 10:03:51 +00:00
260 lines
10 KiB
Rust
260 lines
10 KiB
Rust
use std::collections::BTreeMap;
|
|
|
|
use craft_ecproj_format::{File, Head, ReadError, SCHEMA_VERSION, check_schema};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Debug, Default, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct View {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
active_comp: Option<u64>,
|
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
open_comps: Vec<u64>,
|
|
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
|
comp_times: BTreeMap<u64, i64>,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
|
struct Settings {
|
|
bit_depth: String,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
|
struct Item {
|
|
id: u64,
|
|
name: String,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
|
struct Project {
|
|
schema: u32,
|
|
settings: Settings,
|
|
items: BTreeMap<u64, Item>,
|
|
next_id: u64,
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
render_queue: Vec<u64>,
|
|
}
|
|
|
|
fn project() -> Project {
|
|
Project {
|
|
schema: 2,
|
|
settings: Settings { bit_depth: "Bpc8".into() },
|
|
items: BTreeMap::from([(1, Item { id: 1, name: "Comp 1".into() }), (2, Item { id: 2, name: "clip.mov".into() })]),
|
|
next_id: 3,
|
|
render_queue: vec![],
|
|
}
|
|
}
|
|
|
|
const PROJECT_MEMBERS: &str = r#" "schema": 2,
|
|
"settings": {
|
|
"bit_depth": "Bpc8"
|
|
},
|
|
"items": {
|
|
"1": {
|
|
"id": 1,
|
|
"name": "Comp 1"
|
|
},
|
|
"2": {
|
|
"id": 2,
|
|
"name": "clip.mov"
|
|
}
|
|
},
|
|
"next_id": 3
|
|
}"#;
|
|
|
|
#[test]
|
|
fn a_file_with_a_saved_view_holds_saved_by_then_the_view_then_the_project() {
|
|
let view = View { active_comp: Some(1), open_comps: vec![1, 2], comp_times: BTreeMap::from([(1, 254_016_000_000)]) };
|
|
let p = project();
|
|
let text = File::new("0.12.0", &view, &p).to_json().unwrap();
|
|
let expected = format!(
|
|
r#"{{
|
|
"savedBy": "0.12.0",
|
|
"activeComp": 1,
|
|
"openComps": [
|
|
1,
|
|
2
|
|
],
|
|
"compTimes": {{
|
|
"1": 254016000000
|
|
}},
|
|
{PROJECT_MEMBERS}"#
|
|
);
|
|
assert_eq!(text, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn a_file_with_an_empty_view_holds_saved_by_then_the_project() {
|
|
let p = project();
|
|
let text = File::new("0.12.0", &View::default(), &p).to_json().unwrap();
|
|
assert_eq!(text, format!("{{\n \"savedBy\": \"0.12.0\",\n{PROJECT_MEMBERS}"));
|
|
}
|
|
|
|
#[test]
|
|
fn a_compact_file_holds_the_same_members_on_one_line() {
|
|
let view = View { active_comp: Some(1), open_comps: vec![1, 2], comp_times: BTreeMap::from([(1, 254_016_000_000)]) };
|
|
let p = project();
|
|
let text = File::new("0.12.0", &view, &p).to_json_compact().unwrap();
|
|
assert_eq!(
|
|
text,
|
|
r#"{"savedBy":"0.12.0","activeComp":1,"openComps":[1,2],"compTimes":{"1":254016000000},"schema":2,"settings":{"bit_depth":"Bpc8"},"items":{"1":{"id":1,"name":"Comp 1"},"2":{"id":2,"name":"clip.mov"}},"next_id":3}"#
|
|
);
|
|
assert_eq!(serde_json::from_str::<Project>(&text).unwrap(), p);
|
|
assert_eq!(Head::read(&text).unwrap(), Head { saved_by: Some("0.12.0".into()), schema: 2 });
|
|
let pretty = File::new("0.12.0", &view, &p).to_json().unwrap();
|
|
assert_eq!(serde_json::from_str::<serde_json::Value>(&text).unwrap(), serde_json::from_str::<serde_json::Value>(&pretty).unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn a_written_file_reads_back_as_the_project_and_its_head() {
|
|
let p = project();
|
|
let view = View { active_comp: Some(1), ..View::default() };
|
|
let text = File::new("0.12.0", &view, &p).to_json().unwrap();
|
|
assert_eq!(serde_json::from_str::<Project>(&text).unwrap(), p);
|
|
assert_eq!(Head::read(&text).unwrap(), Head { saved_by: Some("0.12.0".into()), schema: 2 });
|
|
}
|
|
|
|
#[test]
|
|
fn a_project_saved_before_saved_by_has_a_head_without_it() {
|
|
let text = r#"{"schema": 1, "settings": {}, "items": {}, "next_id": 1}"#;
|
|
assert_eq!(Head::read(text).unwrap(), Head { saved_by: None, schema: 1 });
|
|
}
|
|
|
|
#[test]
|
|
fn reader_heads_preserve_string_reader_behavior() {
|
|
for text in [
|
|
r#"{"schema":2,"savedBy":"writer","items":{}}"#,
|
|
r#"{"schema":99,"savedBy":"new writer"}"#,
|
|
r#"{"schema":2,"savedBy":"first","savedBy":"second"}"#,
|
|
r#"{"schema":2,"schema":2}"#,
|
|
r#"{"schema":"bad"}"#,
|
|
"[]",
|
|
"",
|
|
] {
|
|
match (Head::read_from(text.as_bytes()), Head::read(text)) {
|
|
(Ok(from_reader), Ok(from_str)) => assert_eq!(from_reader, from_str),
|
|
(Err(ReadError::Json(from_reader)), Err(ReadError::Json(from_str))) => {
|
|
assert_eq!(from_reader.classify(), from_str.classify());
|
|
// serde's reader may consume lookahead before reporting the same error.
|
|
assert_eq!(from_reader.to_string().split(" at line ").next(), from_str.to_string().split(" at line ").next());
|
|
}
|
|
(Err(ReadError::TooNew { too_new: a, saved_by: a_writer, .. }), Err(ReadError::TooNew { too_new: b, saved_by: b_writer, .. })) => {
|
|
assert_eq!(a.schema, b.schema);
|
|
assert_eq!(a_writer, b_writer);
|
|
}
|
|
(from_reader, from_str) => panic!("different header results: {from_reader:?}, {from_str:?}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn reader_heads_propagate_an_interruption_before_consuming_all_input() {
|
|
struct Interrupted {
|
|
input: std::io::Cursor<Vec<u8>>,
|
|
left: usize,
|
|
}
|
|
impl std::io::Read for Interrupted {
|
|
fn read(&mut self, bytes: &mut [u8]) -> std::io::Result<usize> {
|
|
if self.left == 0 {
|
|
return Err(std::io::Error::other("stopped"));
|
|
}
|
|
let len = bytes.len().min(self.left);
|
|
let n = std::io::Read::read(&mut self.input, &mut bytes[..len])?;
|
|
self.left -= n;
|
|
Ok(n)
|
|
}
|
|
}
|
|
let json = format!(r#"{{"schema":2,"padding":"{}"}}"#, "x".repeat(128 * 1024));
|
|
let mut reader = Interrupted { input: std::io::Cursor::new(json.into_bytes()), left: 32 * 1024 };
|
|
assert!(matches!(Head::read_from(&mut reader), Err(ReadError::Json(e)) if e.is_io()));
|
|
assert_eq!(reader.input.position(), 32 * 1024);
|
|
}
|
|
|
|
#[test]
|
|
fn a_head_needs_a_schema() {
|
|
assert!(Head::read(r#"{"savedBy": "0.12.0", "items": {}}"#).is_err());
|
|
assert!(Head::read(r#"{"savedBy": "0.12.0", "schema": "2"}"#).is_err());
|
|
assert!(Head::read(r#"{"savedBy": "0.13.0", "schema": null, "schema": "3"}"#).is_err());
|
|
assert!(Head::read("[]").is_err());
|
|
assert!(Head::read(r#"["0.12.0", 2]"#).is_err());
|
|
assert!(Head::read(r#"{"schema": 2, "schema": 2}"#).is_err());
|
|
assert!(Head::read(r#"{"schema": 2} {}"#).is_err());
|
|
assert!(Head::read(r#"{"savedBy": "0.12.0", "schema": 2"#).is_err());
|
|
assert!(Head::read("").is_err());
|
|
assert!(Head::read("2").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn a_head_reads_a_saved_by_that_is_not_one_string_as_none() {
|
|
assert_eq!(Head::read(r#"{"savedBy": 12, "schema": 2}"#).unwrap(), Head { saved_by: None, schema: 2 });
|
|
assert_eq!(Head::read(r#"{"savedBy": ["0.12.0"], "schema": 2}"#).unwrap(), Head { saved_by: None, schema: 2 });
|
|
assert_eq!(Head::read(r#"{"savedBy": {"v": "0.12.0"}, "schema": 2}"#).unwrap(), Head { saved_by: None, schema: 2 });
|
|
assert_eq!(Head::read(r#"{"savedBy": "a", "savedBy": "b", "schema": 2}"#).unwrap(), Head { saved_by: None, schema: 2 });
|
|
assert_eq!(Head::read(r#"{"savedBy": "a", "schema": 2, "savedBy": 1, "savedBy": "c"}"#).unwrap(), Head { saved_by: None, schema: 2 });
|
|
assert_eq!(Head::read(r#"{"schema": 2, "savedBy": "0.13.0-dev"}"#).unwrap(), Head { saved_by: Some("0.13.0-dev".into()), schema: 2 });
|
|
}
|
|
|
|
#[test]
|
|
fn a_head_reads_the_object_form_only() {
|
|
assert!(matches!(Head::read(r#"[2, {}, {}, 1]"#), Err(ReadError::Json(_))));
|
|
assert!(matches!(Head::read("[1]"), Err(ReadError::Json(_))));
|
|
assert!(matches!(Head::read(r#"["savedBy", "0.12.0"]"#), Err(ReadError::Json(_))));
|
|
}
|
|
|
|
#[test]
|
|
fn a_head_refuses_a_newer_schema() {
|
|
let Err(ReadError::TooNew { too_new, saved_by, .. }) = Head::read(r#"{"savedBy": "0.13.0", "schema": 3}"#) else {
|
|
panic!("schema 3 is newer than 2")
|
|
};
|
|
assert_eq!((too_new.schema, too_new.supported), (3, 2));
|
|
assert_eq!(saved_by.as_deref(), Some("0.13.0"));
|
|
let Err(e) = Head::read(r#"{"schema": 4294967295, "savedBy": 1}"#) else { panic!("schema u32::MAX is newer than 2") };
|
|
assert_eq!(e.to_string(), "project schema 4294967295 is newer than this version supports (2)");
|
|
let ReadError::TooNew { saved_by, .. } = e else { panic!("{e:?} is not TooNew") };
|
|
assert_eq!(saved_by, None);
|
|
assert_eq!(Head::read(r#"{"savedBy": "0.12.0", "schema": 2}"#).unwrap(), Head { saved_by: Some("0.12.0".into()), schema: 2 });
|
|
}
|
|
|
|
#[test]
|
|
fn a_head_skips_other_members_of_any_type() {
|
|
let text = r#"{"items": [1, {"schema": 9}], "savedBy": null, "schema": 2, "next_id": 1.5}"#;
|
|
assert_eq!(Head::read(text).unwrap(), Head { saved_by: None, schema: 2 });
|
|
}
|
|
|
|
#[test]
|
|
fn a_deeply_nested_saved_by_is_skipped_like_other_members() {
|
|
let deep = format!("{}{}", "[".repeat(10_000), "]".repeat(10_000));
|
|
let text = format!(r#"{{"savedBy": {deep}, "schema": 2, "items": {deep}}}"#);
|
|
assert_eq!(Head::read(&text).unwrap(), Head { saved_by: None, schema: 2 });
|
|
let deep_map = format!("{}0{}", r#"{"v": "#.repeat(10_000), "}".repeat(10_000));
|
|
let text = format!(r#"{{"savedBy": "0.12.0", "savedBy": {deep_map}, "schema": 2}}"#);
|
|
assert_eq!(Head::read(&text).unwrap(), Head { saved_by: None, schema: 2 });
|
|
}
|
|
|
|
#[test]
|
|
fn a_wide_saved_by_array_reads_as_none() {
|
|
let wide = format!("[{}0]", "0,".repeat(1_000_000));
|
|
let text = format!(r#"{{"savedBy": {wide}, "schema": 2, "savedBy": {wide}}}"#);
|
|
assert_eq!(Head::read(&text).unwrap(), Head { saved_by: None, schema: 2 });
|
|
let text = format!(r#"{{"schema": 2, "savedBy": "0.12.0", "savedBy": "{}"}}"#, "9".repeat(1_000_000));
|
|
assert_eq!(Head::read(&text).unwrap(), Head { saved_by: None, schema: 2 });
|
|
}
|
|
|
|
#[test]
|
|
fn check_schema_refuses_only_a_newer_schema() {
|
|
assert_eq!(SCHEMA_VERSION, 2);
|
|
assert_eq!(check_schema(0), Ok(()));
|
|
assert_eq!(check_schema(1), Ok(()));
|
|
assert_eq!(check_schema(2), Ok(()));
|
|
for schema in [3, u32::MAX] {
|
|
let Err(e) = check_schema(schema) else { panic!("schema {schema} is newer than 2") };
|
|
assert_eq!((e.schema, e.supported), (schema, 2));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn too_new_reads_as_effectcraft_words_it() {
|
|
let Err(e) = check_schema(3) else { panic!("schema 3 is newer than 2") };
|
|
assert_eq!(e.to_string(), "project schema 3 is newer than this version supports (2)");
|
|
}
|