use super::*;
#[test]
fn light_parse_keeps_proc_body_as_single_item() {
let parse =
parse_light_source("global proc foo() {\nsetAttr \".tx\" 1;\n}\nsetAttr \".ty\" 2;\n");
assert!(parse.errors.is_empty());
assert_eq!(parse.source.items.len(), 2);
let LightItem::Proc(proc_def) = &parse.source.items[0] else {
panic!("expected proc item");
};
assert!(proc_def.is_global);
assert_eq!(
proc_def.name_range.map(|range| parse.source_slice(range)),
Some("foo")
);
let LightItem::Command(command) = &parse.source.items[1] else {
panic!("expected command item");
};
assert_eq!(parse.source_slice(command.head_range), "setAttr");
}
#[test]
fn shared_light_parse_reuses_arc_text_and_matches_owned_parse() {
let source: Arc<str> = Arc::from("global proc foo() { }\nsetAttr \".tx\" 1;\n");
let parse = parse_light_shared_source(Arc::clone(&source));
let owned = parse_light_source(source.as_ref());
assert!(Arc::ptr_eq(&parse.source_text, &source));
assert_eq!(parse.source, owned.source);
assert_eq!(parse.source_map, owned.source_map);
assert_eq!(parse.errors, owned.errors);
let LightItem::Command(command) = &parse.source.items[1] else {
panic!("expected command item");
};
assert_eq!(parse.source_slice(command.head_range), "setAttr");
}
#[test]
fn shared_light_parse_bytes_matches_owned_utf8_bytes_path() {
let input = b"global proc foo() { }\nsetAttr \".tx\" 1;\n";
let parse = parse_light_shared_bytes(input);
let owned = parse_light_bytes(input);
assert_eq!(parse.source, owned.source);
assert_eq!(parse.source_map, owned.source_map);
assert_eq!(parse.source_encoding, owned.source_encoding);
assert_eq!(parse.decode_errors, owned.decode_errors);
assert_eq!(parse.errors, owned.errors);
assert_eq!(parse.source_text.as_ref(), owned.source_text);
}
#[test]
fn shared_light_parse_bytes_with_encoding_matches_owned_cp932_path() {
let (bytes, _, _) = SHIFT_JIS.encode("setAttr \".名\" -type \"string\" \"値\";\n");
let parse = parse_light_shared_bytes_with_encoding(bytes.as_ref(), SourceEncoding::Cp932);
let owned = parse_light_bytes_with_encoding(bytes.as_ref(), SourceEncoding::Cp932);
assert_eq!(parse.source, owned.source);
assert_eq!(parse.source_map, owned.source_map);
assert_eq!(parse.source_encoding, owned.source_encoding);
assert_eq!(parse.decode_errors, owned.decode_errors);
assert_eq!(parse.errors, owned.errors);
let LightItem::Command(command) = &parse.source.items[0] else {
panic!("expected command item");
};
assert_eq!(parse.source_slice(command.words[0].range()), "\".名\"");
}
#[test]
fn streaming_light_scan_matches_materialized_items() {
let source = "global proc foo() { }\nsetAttr \".tx\" 1;\n";
let materialized = parse_light_source(source);
let mut streamed = Vec::new();
let report = scan_light_source_with_options_and_sink(
source,
LightParseOptions::default(),
&mut |_: mel_syntax::SourceView<'_>, item: LightItem| streamed.push(item),
);
assert_eq!(streamed, materialized.source.items);
assert_eq!(report.errors, materialized.errors);
}
#[test]
fn streaming_shared_light_scan_matches_materialized_items() {
let source: Arc<str> = Arc::from("global proc foo() { }\nsetAttr \".tx\" 1;\n");
let materialized = parse_light_shared_source(Arc::clone(&source));
let mut streamed = Vec::new();
let report = scan_light_shared_source_with_options_and_sink(
Arc::clone(&source),
LightParseOptions::default(),
&mut |_: mel_syntax::SourceView<'_>, item: LightItem| streamed.push(item),
);
assert!(Arc::ptr_eq(&report.source_text, &source));
assert_eq!(streamed, materialized.source.items);
assert_eq!(report.errors, materialized.errors);
}
#[test]
fn streaming_shared_light_scan_bytes_matches_materialized_items() {
let (bytes, _, _) = SHIFT_JIS.encode("setAttr \".名\" -type \"string\" \"値\";\n");
let materialized =
parse_light_shared_bytes_with_encoding(bytes.as_ref(), SourceEncoding::Cp932);
let mut streamed = Vec::new();
let report = scan_light_shared_bytes_with_encoding_and_options_and_sink(
bytes.as_ref(),
SourceEncoding::Cp932,
LightParseOptions::default(),
&mut |_: mel_syntax::SourceView<'_>, item: LightItem| streamed.push(item),
);
assert_eq!(streamed, materialized.source.items);
assert_eq!(report.errors, materialized.errors);
let LightItem::Command(command) = &streamed[0] else {
panic!("expected command item");
};
assert_eq!(report.source_slice(command.words[0].range()), "\".名\"");
}
#[test]
fn streaming_shared_light_scan_utf8_bytes_matches_materialized_items() {
let source = b"global proc foo() { }\nsetAttr \".tx\" 1;\n";
let materialized = parse_light_shared_bytes(source);
let mut streamed = Vec::new();
let report = scan_light_shared_bytes_with_options_and_sink(
source,
LightParseOptions::default(),
&mut |_: mel_syntax::SourceView<'_>, item: LightItem| streamed.push(item),
);
assert_eq!(streamed, materialized.source.items);
assert_eq!(report.errors, materialized.errors);
}
#[test]
fn parse_light_shared_file_matches_owned_utf8_file_path() {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after unix epoch")
.as_nanos();
let path = std::env::temp_dir().join(format!("mel-parser-light-shared-utf8-{unique}.mel"));
fs::write(&path, "global proc foo() { }\nsetAttr \".tx\" 1;\n")
.expect("temp fixture should be writable");
let parse = parse_light_shared_file(&path).expect("shared light parse should succeed");
let owned = parse_light_file(&path).expect("owned light parse should succeed");
fs::remove_file(&path).expect("temp fixture should be removable");
assert_eq!(parse.source, owned.source);
assert_eq!(parse.source_map, owned.source_map);
assert_eq!(parse.source_encoding, owned.source_encoding);
assert_eq!(parse.decode_errors, owned.decode_errors);
assert_eq!(parse.errors, owned.errors);
assert_eq!(parse.source_text.as_ref(), owned.source_text);
}
#[test]
fn parse_light_file_with_options_respects_max_bytes_budget() {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after unix epoch")
.as_nanos();
let path = std::env::temp_dir().join(format!("mel-parser-light-budget-file-{unique}.mel"));
fs::write(&path, "setAttr \".tx\" 1;\n").expect("temp fixture should be writable");
let parse = parse_light_file_with_options(
&path,
LightParseOptions {
budgets: ParseBudgets {
max_bytes: 4,
..ParseBudgets::default()
},
..LightParseOptions::default()
},
)
.expect("budgeted light parse should succeed");
fs::remove_file(&path).expect("temp fixture should be removable");
assert!(parse.source.items.is_empty());
assert_eq!(parse.errors.len(), 1);
assert_eq!(
parse.errors[0].message,
"source exceeds parse budget: max_bytes"
);
}
#[test]
fn scan_light_shared_file_with_encoding_matches_owned_cp932_file_path() {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after unix epoch")
.as_nanos();
let path = std::env::temp_dir().join(format!("mel-parser-light-shared-cp932-{unique}.mel"));
let (bytes, _, _) = SHIFT_JIS.encode("setAttr \".名\" -type \"string\" \"値\";\n");
fs::write(&path, bytes.as_ref()).expect("temp fixture should be writable");
let mut shared_items = Vec::new();
let shared = scan_light_shared_file_with_encoding_and_options_and_sink(
&path,
SourceEncoding::Cp932,
LightParseOptions::default(),
&mut |_: mel_syntax::SourceView<'_>, item: LightItem| shared_items.push(item),
)
.expect("shared light scan should succeed");
let mut owned_items = Vec::new();
let owned = scan_light_file_with_encoding_and_options_and_sink(
&path,
SourceEncoding::Cp932,
LightParseOptions::default(),
&mut |_: mel_syntax::SourceView<'_>, item: LightItem| owned_items.push(item),
)
.expect("owned light scan should succeed");
fs::remove_file(&path).expect("temp fixture should be removable");
assert_eq!(shared_items, owned_items);
assert_eq!(shared.source_map, owned.source_map);
assert_eq!(shared.source_encoding, owned.source_encoding);
assert_eq!(shared.decode_errors, owned.decode_errors);
assert_eq!(shared.errors, owned.errors);
assert_eq!(shared.source_text.as_ref(), owned.source_text);
}
#[test]
fn parse_light_file_with_encoding_and_options_respects_max_bytes_budget() {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after unix epoch")
.as_nanos();
let path = std::env::temp_dir().join(format!(
"mel-parser-light-budget-file-encoding-{unique}.mel"
));
fs::write(&path, b"setAttr \".tx\" 1;\n").expect("temp fixture should be writable");
let parse = parse_light_file_with_encoding_and_options(
&path,
SourceEncoding::Utf8,
LightParseOptions {
budgets: ParseBudgets {
max_bytes: 4,
..ParseBudgets::default()
},
..LightParseOptions::default()
},
)
.expect("budgeted encoded light parse should succeed");
fs::remove_file(&path).expect("temp fixture should be removable");
assert!(parse.source.items.is_empty());
assert_eq!(parse.errors.len(), 1);
assert_eq!(
parse.errors[0].message,
"source exceeds parse budget: max_bytes"
);
}
#[test]
fn light_parse_tracks_multiline_command_tail_as_single_statement() {
let source = "setAttr \".fc[0]\" -type \"polyFaces\"\n f 4 0 1 2 3\n mu 0 4 0 1 2 3;\n";
let parse = parse_light_source_with_options(
source,
LightParseOptions {
max_prefix_words: 4,
max_prefix_bytes: 48,
..LightParseOptions::default()
},
);
assert!(parse.errors.is_empty());
let LightItem::Command(command) = &parse.source.items[0] else {
panic!("expected command item");
};
assert_eq!(parse.source_slice(command.head_range), "setAttr");
assert!(command.opaque_tail.is_some());
assert_eq!(parse.source.items.len(), 1);
let opaque_tail = parse.source_slice(command.opaque_tail.expect("opaque tail"));
assert!(opaque_tail.starts_with("4 0 1 2 3"));
assert!(opaque_tail.contains("mu 0 4 0 1 2 3"));
}
#[test]
fn light_parse_bounds_prefix_words_for_large_payloads() {
let source = "setAttr \".pt\" 1 2 3 4 5 6 7 8 9 10;\n";
let parse = parse_light_source_with_options(
source,
LightParseOptions {
max_prefix_words: 3,
max_prefix_bytes: 24,
..LightParseOptions::default()
},
);
assert!(parse.errors.is_empty());
let LightItem::Command(command) = &parse.source.items[0] else {
panic!("expected command item");
};
assert_eq!(command.words.len(), 3);
assert!(matches!(command.words[0], LightWord::QuotedString { .. }));
assert!(matches!(command.words[1], LightWord::NumericLiteral { .. }));
assert!(command.opaque_tail.is_some());
}
#[test]
fn light_parse_bytes_preserves_safe_source_slices_for_non_utf8() {
let (bytes, _, _) = SHIFT_JIS.encode("setAttr \".名\" -type \"string\" \"値\";\n");
let parse = parse_light_bytes(bytes.as_ref());
assert!(parse.errors.is_empty());
let LightItem::Command(command) = &parse.source.items[0] else {
panic!("expected command item");
};
assert_eq!(parse.source_slice(command.head_range), "setAttr");
assert_eq!(parse.source_slice(command.words[0].range()), "\".名\"");
}
#[test]
fn streaming_light_scan_bytes_preserves_safe_source_slices_for_non_utf8() {
let (bytes, _, _) = SHIFT_JIS.encode("setAttr \".名\" -type \"string\" \"値\";\n");
let mut streamed = Vec::new();
let report = scan_light_bytes_with_sink(
bytes.as_ref(),
&mut |_: mel_syntax::SourceView<'_>, item: LightItem| streamed.push(item),
);
assert!(report.errors.is_empty());
let LightItem::Command(command) = &streamed[0] else {
panic!("expected command item");
};
assert_eq!(report.source_slice(command.head_range), "setAttr");
assert_eq!(report.source_slice(command.words[0].range()), "\".名\"");
}
#[test]
fn no_retain_light_scan_bytes_matches_retained_utf8_path() {
let source = b"global proc foo() { }\nsetAttr \".tx\" 1;\n";
let materialized = parse_light_bytes(source);
let streamed = RefCell::new(Vec::new());
let (head, summary) = scan_light_bytes_with_options_and_sink_and_then(
source,
LightParseOptions::default(),
&mut |_: mel_syntax::SourceView<'_>, item: LightItem| streamed.borrow_mut().push(item),
|_, source: mel_syntax::SourceView<'_>, summary| {
let items = streamed.borrow();
let LightItem::Command(command) = &items[1] else {
panic!("expected command item");
};
(source.slice(command.head_range).to_string(), summary)
},
);
assert_eq!(streamed.into_inner(), materialized.source.items);
assert_eq!(summary.errors, materialized.errors);
assert_eq!(summary.decode_errors, materialized.decode_errors);
assert_eq!(summary.source_encoding, materialized.source_encoding);
assert_eq!(head, "setAttr");
}
#[test]
fn no_retain_light_scan_bytes_preserves_non_utf8_source_slices() {
let (bytes, _, _) = SHIFT_JIS.encode("setAttr \".名\" -type \"string\" \"値\";\n");
let materialized = parse_light_bytes(bytes.as_ref());
let streamed = RefCell::new(Vec::new());
let (first_arg, summary) = scan_light_bytes_with_options_and_sink_and_then(
bytes.as_ref(),
LightParseOptions::default(),
&mut |_: mel_syntax::SourceView<'_>, item: LightItem| streamed.borrow_mut().push(item),
|_, source: mel_syntax::SourceView<'_>, summary| {
let items = streamed.borrow();
let LightItem::Command(command) = &items[0] else {
panic!("expected command item");
};
(source.slice(command.words[0].range()).to_string(), summary)
},
);
assert_eq!(streamed.into_inner(), materialized.source.items);
assert_eq!(summary.errors, materialized.errors);
assert_eq!(summary.decode_errors, materialized.decode_errors);
assert_eq!(summary.source_encoding, materialized.source_encoding);
assert_eq!(first_arg, "\".名\"");
}
#[test]
fn no_retain_light_scan_reports_budget_error_without_items() {
let streamed = RefCell::new(Vec::new());
let summary = scan_light_bytes_with_options_and_sink_and_then(
b"setAttr \".tx\" 1;\n",
LightParseOptions {
budgets: ParseBudgets {
max_bytes: 4,
..ParseBudgets::default()
},
..LightParseOptions::default()
},
&mut |_: mel_syntax::SourceView<'_>, item: LightItem| streamed.borrow_mut().push(item),
|_, source: mel_syntax::SourceView<'_>, summary| {
assert_eq!(source.text(), "");
summary
},
);
assert!(streamed.into_inner().is_empty());
assert_eq!(summary.decode_errors, Vec::<DecodeDiagnostic>::new());
assert_eq!(summary.errors.len(), 1);
assert_eq!(
summary.errors[0].message,
"source exceeds parse budget: max_bytes"
);
}
#[test]
fn light_parse_reports_max_bytes_budget_before_scan_starts() {
let parse = parse_light_source_with_options(
"setAttr \".tx\" 1;\n",
LightParseOptions {
budgets: ParseBudgets {
max_bytes: 4,
..ParseBudgets::default()
},
..LightParseOptions::default()
},
);
assert!(parse.source.items.is_empty());
assert_eq!(parse.errors.len(), 1);
assert_eq!(
parse.errors[0].message,
"source exceeds parse budget: max_bytes"
);
}
#[test]
fn light_parse_reports_max_statements_budget_and_drops_tail_items() {
let parse = parse_light_source_with_options(
"setAttr \".tx\" 1;\nsetAttr \".ty\" 2;\n",
LightParseOptions {
budgets: ParseBudgets {
max_statements: 1,
..ParseBudgets::default()
},
..LightParseOptions::default()
},
);
assert_eq!(parse.source.items.len(), 1);
assert_eq!(parse.errors.len(), 1);
assert_eq!(
parse.errors[0].message,
"source exceeds parse budget: max_statements"
);
}
#[test]
fn light_parse_reports_max_literal_bytes_budget_for_string_word() {
let parse = parse_light_source_with_options(
"setAttr \".tx\" \"abcdef\";\n",
LightParseOptions {
budgets: ParseBudgets {
max_literal_bytes: 4,
..ParseBudgets::default()
},
..LightParseOptions::default()
},
);
assert_eq!(parse.errors.len(), 1);
assert_eq!(
parse.errors[0].message,
"source exceeds parse budget: max_literal_bytes"
);
}
#[test]
fn light_parse_reports_max_nesting_depth_budget_for_grouped_expr() {
let parse = parse_light_source_with_options(
"file -command ((((\"x\"))));\n",
LightParseOptions {
budgets: ParseBudgets {
max_nesting_depth: 2,
..ParseBudgets::default()
},
..LightParseOptions::default()
},
);
assert_eq!(parse.errors.len(), 1);
assert_eq!(
parse.errors[0].message,
"source exceeds parse budget: max_nesting_depth"
);
}
#[test]
fn light_parse_reports_max_tokens_budget_for_long_command() {
let parse = parse_light_source_with_options(
"setAttr \".pt\" 1 2 3 4 5 6;\n",
LightParseOptions {
budgets: ParseBudgets {
max_tokens: 6,
..ParseBudgets::default()
},
..LightParseOptions::default()
},
);
assert_eq!(parse.errors.len(), 1);
assert_eq!(
parse.errors[0].message,
"source exceeds parse budget: max_tokens"
);
}
#[test]
fn light_parse_does_not_double_count_command_head_tokens_against_budget() {
let parse = parse_light_source_with_options(
"createNode -n foo;\n",
LightParseOptions {
budgets: ParseBudgets {
max_tokens: 4,
..ParseBudgets::default()
},
..LightParseOptions::default()
},
);
assert_eq!(parse.source.items.len(), 1);
assert!(parse.errors.is_empty());
}
#[test]
fn light_parse_does_not_double_count_global_statement_proc_probe_tokens() {
let parse = parse_light_source_with_options(
"global int $x;\n",
LightParseOptions {
budgets: ParseBudgets {
max_tokens: 4,
..ParseBudgets::default()
},
..LightParseOptions::default()
},
);
assert_eq!(parse.source.items.len(), 1);
assert!(parse.errors.is_empty());
}