use super::{detect_tail_format, parse_sections};
use crate::rule::frx::LoadError;
use forbidden_regex::RegexSet;
#[test]
fn single_line_bare_literal_section_is_boundary_gated_and_named() {
let rules = parse_sections("==> qqq-token <==\nabc").expect("one literal rule");
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].name.as_deref(), Some("qqq-token"));
assert_eq!(rules[0].pattern, "\\babc\\b");
}
#[test]
fn single_line_regex_section_passes_body_and_retains_name() {
let rules = parse_sections("==> qqq-re <==\n/foo[0-9]/mx").expect("one regex rule");
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].name.as_deref(), Some("qqq-re"));
assert_eq!(rules[0].pattern, "foo[0-9]");
let set = RegexSet::new(std::slice::from_ref(&rules[0].pattern)).expect("compile regex");
assert!(set.is_match(b"foo7"));
}
#[test]
fn multi_line_section_with_first_column_comments_compiles_and_matches() {
let text = concat!(
"==> qqq-multi <==\n",
"# the alpha marker\n",
"alpha\n",
"# then a trailing digit\n",
"[0-9]\n",
);
let rules = parse_sections(text).expect("multi-line section");
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].name.as_deref(), Some("qqq-multi"));
assert!(rules[0].pattern.contains("# the alpha marker"), "{}", rules[0].pattern);
assert!(rules[0].pattern.contains("# then a trailing digit"), "{}", rules[0].pattern);
let set = RegexSet::new(std::slice::from_ref(&rules[0].pattern)).expect("compile multi-line");
assert!(set.is_match(b"see alpha5 here"));
assert!(!set.is_match(b"see alpha here"));
assert!(!set.is_match(b"see beta5 here"));
}
#[test]
fn reshape_convention_matches_header_text_through_regexset() {
let text = concat!(
"==> qqq-reshape <==\n",
"# reshaped so this stays body, not a header\n",
"[=]=>foo\n",
"# a trailing digit keeps this a multi-line verbatim body\n",
"[0-9]\n",
);
let rules = parse_sections(text).expect("reshape section");
assert_eq!(rules.len(), 1);
let set = RegexSet::new(std::slice::from_ref(&rules[0].pattern)).expect("compile reshape");
assert!(set.is_match(b"prefix ==>foo7 suffix"), "{}", rules[0].pattern);
assert!(!set.is_match(b"prefix ==>foobar suffix"), "{}", rules[0].pattern);
}
#[test]
fn out_of_alphabet_arrow_lines_fail_closed_as_near_headers() {
let upper = parse_sections("==> qqq-carve <==\ncontent-a\n==> Upper9 <==\n")
.expect_err("uppercase middle is a near-header");
assert_eq!(upper, LoadError::NearHeader { line: 3 });
let slash = parse_sections("==> qqq-carve <==\ncontent-a\n==> path/seg <==\n")
.expect_err("slash middle is a near-header");
assert_eq!(slash, LoadError::NearHeader { line: 3 });
}
#[test]
fn indented_exact_header_fails_closed_as_near_header() {
let err = parse_sections("==> qqq-outer <==\nbody-line\n ==> qqq-inner <==\nx\n")
.expect_err("indented header");
assert_eq!(err, LoadError::NearHeader { line: 3 });
}
#[test]
fn arrow_without_space_fails_closed_as_near_header() {
let err = parse_sections("==> qqq-outer <==\nbody-line\n==>not-a-header\n")
.expect_err("no-space arrow");
assert_eq!(err, LoadError::NearHeader { line: 3 });
}
#[test]
fn comment_lines_mentioning_header_text_are_unaffected() {
let text = concat!(
"# tail writes ==> name <== headers\n",
"==> qqq-cmt <==\n",
"# see ==> qqq-cmt <== for this rule\n",
"content-a\n",
);
let rules = parse_sections(text).expect("comment arrows stay comments");
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].name.as_deref(), Some("qqq-cmt"));
}
#[test]
fn reshape_bytes_in_bare_literal_stay_literal() {
let rules = parse_sections("==> qqq-lit <==\n[=]=> marker\n").expect("literal rule");
assert_eq!(rules.len(), 1);
let set = RegexSet::new(std::slice::from_ref(&rules[0].pattern)).expect("compile literal");
assert!(set.is_match(b"see [=]=> marker here"), "{}", rules[0].pattern);
assert!(!set.is_match(b"see ==> marker here"), "{}", rules[0].pattern);
}
#[test]
fn near_header_fails_closed_with_line_number() {
let err = parse_sections("==> qqq-ok <==\nabcdefgh\n==> -bad <==\nx")
.expect_err("near header");
assert_eq!(err, LoadError::NearHeader { line: 3 });
}
#[test]
fn duplicate_name_fails_closed_with_both_lines() {
let err = parse_sections("==> qqq-dup <==\naaaa\n==> qqq-dup <==\nbbbb\n")
.expect_err("duplicate name");
assert_eq!(err, LoadError::DuplicateName { first_line: 1, line: 3 });
}
#[test]
fn empty_section_fails_closed_with_header_line() {
let err = parse_sections("==> qqq-empty <==\n\n# only a comment\n\n==> qqq-next <==\nzzzz\n")
.expect_err("empty section");
assert_eq!(err, LoadError::EmptySection { line: 1 });
}
#[test]
fn pre_header_content_fails_closed_with_line_number() {
let err = parse_sections("stray content\n==> qqq-late <==\nbody\n")
.expect_err("pre-header content");
assert_eq!(err, LoadError::PreHeaderContent { line: 1 });
}
#[test]
fn blanks_and_comments_before_first_header_are_ignored() {
let rules = parse_sections("\n# leading comment\n\n==> qqq-lead <==\nzzzz\n")
.expect("leading blanks and comments ignored");
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].name.as_deref(), Some("qqq-lead"));
}
#[test]
fn trailing_blank_lines_are_insignificant() {
let rules = parse_sections("==> qqq-trail <==\n/foo[0-9]/\n\n\n")
.expect("trailing blanks trimmed");
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].pattern, "foo[0-9]");
}
#[test]
fn section_order_is_file_order_with_deterministic_indices() {
let rules = parse_sections("==> qqq-a <==\naaaa\n==> qqq-b <==\nbbbb\n==> qqq-c <==\ncccc\n")
.expect("three sections");
let names: Vec<Option<&str>> = rules.iter().map(|rule| return rule.name.as_deref()).collect();
assert_eq!(names, vec![Some("qqq-a"), Some("qqq-b"), Some("qqq-c")]);
}
#[test]
fn detect_tail_format_selects_the_right_path_both_directions() {
assert!(detect_tail_format("==> qqq-hdr <==\nbody\n"));
assert!(detect_tail_format("\n# comment\n\n==> qqq-hdr <==\nbody\n"));
assert!(!detect_tail_format("=starts-with-equals\nmore\n"));
assert!(!detect_tail_format("plain literal\n"));
assert!(!detect_tail_format("==> Bad-Name <==\nbody\n"));
assert!(!detect_tail_format(""));
}