Files
Marcos Paulo 22b43c1254
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: task-line parser, Templater segmenter, icon/banner property resolver (MiniMax M3.1 Flash)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 07:03:19 -03:00

119 lines
3.4 KiB
Rust

//! Tests of the template segmenter.
use std::time::Instant;
use notescraft_templater::{Error, Kind, MAX_COMMANDS, MAX_TEMPLATE_BYTES, Segment, parse};
/// A text segment.
fn text(value: &str) -> Segment {
Segment::Text(value.to_owned())
}
/// An output command segment.
fn command(src: &str, line: u32) -> Segment {
Segment::Command { src: src.to_owned(), line, kind: Kind::Output }
}
/// The segments of a template that is expected to parse.
fn segments(src: &str) -> Vec<Segment> {
match parse(src) {
Ok(template) => template.segments,
Err(err) => panic!("parse failed: {err}"),
}
}
#[test]
fn plain_text_is_one_segment() {
assert_eq!(segments("hello world"), vec![text("hello world")]);
assert_eq!(segments(""), Vec::new());
}
#[test]
fn one_command() {
assert_eq!(segments("<% x %>"), vec![command("x", 1)]);
assert_eq!(segments("<% x %>"), vec![command("x", 1)]);
}
#[test]
fn three_commands_keep_their_lines() {
assert_eq!(segments("<% a %>\n\n<% b %>\n\n<% c %>"), vec![command("a", 1), text("\n\n"), command("b", 3), text("\n\n"), command("c", 5)]);
}
#[test]
fn minus_before_strips_every_whitespace() {
assert_eq!(segments("a \n <%- x %>"), vec![text("a"), command("x", 2)]);
}
#[test]
fn underscore_before_strips_spaces_and_tabs() {
assert_eq!(segments("a \n <%_ x %>"), vec![text("a \n"), command("x", 2)]);
}
#[test]
fn minus_after_strips_the_newline() {
assert_eq!(segments("<% x -%> \n b"), vec![command("x", 1), text(" b")]);
}
#[test]
fn underscore_after_strips_spaces_and_tabs() {
assert_eq!(segments("<% x _%> \n b"), vec![command("x", 1), text("\n b")]);
}
#[test]
fn a_javascript_command_is_kept_and_marked() {
assert_eq!(segments("<%* tR.now() %>"), vec![Segment::Command { src: "tR.now()".to_owned(), line: 1, kind: Kind::Js }]);
}
#[test]
fn an_unterminated_command_reports_its_line() {
match parse("a\nb\n<% x") {
Err(Error::Unterminated { line }) => assert_eq!(line, 3),
other => panic!("expected an unterminated command, got {other:?}"),
}
}
#[test]
fn a_close_marker_without_an_open_is_text() {
assert_eq!(segments("a %> b"), vec![text("a %> b")]);
}
#[test]
fn carriage_returns_survive_outside_the_stripped_areas() {
assert_eq!(segments("a\r\n<% x %>\r\nb"), vec![text("a\r\n"), command("x", 2), text("\r\nb")]);
}
#[test]
fn minus_after_strips_a_carriage_return_line_feed() {
assert_eq!(segments("<% x -%>\r\nb"), vec![command("x", 1), text("b")]);
}
#[test]
fn too_many_commands_are_refused() {
let src = "<% x %>".repeat(MAX_COMMANDS.saturating_add(1));
match parse(&src) {
Err(Error::TooManyCommands) => {}
other => panic!("expected too many commands, got {other:?}"),
}
let ok = "<% x %>".repeat(MAX_COMMANDS);
assert!(parse(&ok).is_ok());
}
#[test]
fn a_template_over_one_mebibyte_is_refused() {
let src = "a".repeat(MAX_TEMPLATE_BYTES.saturating_add(1));
match parse(&src) {
Err(Error::TooLarge) => {}
other => panic!("expected a too large error, got {other:?}"),
}
}
#[test]
fn a_megabyte_of_open_markers_fails_fast() {
let src = "<%".repeat(MAX_TEMPLATE_BYTES / 2);
let start = Instant::now();
let err = parse(&src).unwrap_err();
let elapsed = start.elapsed();
assert!(matches!(err, Error::Unterminated { .. }), "expected an unterminated command, got {err:?}");
assert!(elapsed.as_millis() < 200, "took {elapsed:?}");
}