use bytes::Bytes;
use esi::{parse, parse_complete};
#[test]
fn test_streaming_parse_incomplete_choose_opening() {
let input = b"<esi:choose>";
let bytes = Bytes::from_static(input);
let result = parse(&bytes);
match result {
Err(nom::Err::Incomplete(_)) => {
}
Ok((remaining, elements)) => {
panic!(
"Expected Incomplete but got Ok with {} elements, remaining: {:?}",
elements.len(),
std::str::from_utf8(remaining)
);
}
Err(e) => {
panic!("Expected Incomplete but got error: {:?}", e);
}
}
}
#[test]
fn test_streaming_parse_incomplete_choose_with_partial_content() {
let input = b"<esi:choose>\n <esi:when test=\"";
let bytes = Bytes::from_static(input);
let result = parse(&bytes);
match result {
Err(nom::Err::Incomplete(_)) => {
}
Err(nom::Err::Error(e)) => {
panic!(
"Incomplete input returned Error({:?}) instead of Incomplete. \
This indicates a parser bug - incomplete input should return Incomplete.",
e.code
);
}
Ok((remaining, elements)) => {
panic!(
"Expected Incomplete but got Ok with {} elements and {} bytes remaining. \
Incomplete input should return Incomplete, not partial results.",
elements.len(),
remaining.len()
);
}
Err(e) => {
panic!("Expected Incomplete but got: {:?}", e);
}
}
}
#[test]
fn test_streaming_parse_complete_choose() {
let input = b"<esi:choose>\n <esi:when test=\"true\">content</esi:when>\n</esi:choose>";
let bytes = Bytes::from_static(input);
let result = parse(&bytes);
match result {
Ok((remaining, elements)) => {
assert_eq!(remaining, b"", "Should consume all input");
assert_eq!(elements.len(), 1, "Should parse one Choose element");
}
Err(nom::Err::Incomplete(_)) => {
}
Err(e) => {
panic!("Expected success or Incomplete, got error: {:?}", e);
}
}
}
#[test]
fn test_parse_complete_vs_parse_on_incomplete_input() {
let input = b"<esi:choose>\n <esi:when test=\"true\">content</esi:when>";
let bytes = Bytes::from_static(input);
let streaming_result = parse(&bytes);
let complete_result = parse_complete(&bytes);
assert!(
matches!(streaming_result, Err(nom::Err::Incomplete(_))),
"Streaming parser should return Incomplete for incomplete input, got: {:?}",
streaming_result
.as_ref()
.map(|(r, e)| (r.len(), e.len()))
.map_err(|e| format!("{:?}", e))
);
match complete_result {
Ok((_remaining, elements)) => {
assert!(
!elements.is_empty(),
"Should parse at least partial content"
);
}
Err(e) => {
panic!("parse_complete unexpectedly failed: {:?}", e);
}
}
}
#[test]
fn test_delimited_propagates_incomplete() {
use nom::bytes::streaming::tag;
use nom::error::Error;
use nom::sequence::delimited;
use nom::Parser;
let input = b"<start>incomplete";
let result: nom::IResult<&[u8], &[u8], Error<&[u8]>> = delimited(
tag(&b"<start>"[..]),
nom::bytes::streaming::take_while1(|c| c != b'<' && c != b'>'),
tag(&b"<end>"[..]),
)
.parse(input);
assert!(
matches!(result, Err(nom::Err::Incomplete(_))),
"delimited() should propagate Incomplete from closing tag parser, got: {:?}",
result
);
}
#[test]
fn test_delimited_with_parse_complete_middle() {
use bytes::Bytes;
let input = Bytes::from_static(b"<esi:choose><esi:when test=\"true\">yes</esi:when>");
let result = parse(&input);
assert!(
matches!(result, Err(nom::Err::Incomplete(_))),
"Expected Incomplete from missing closing tag, got: {:?}",
result
);
}
#[test]
fn test_parse_complete_doesnt_know_boundaries() {
let input = b"<esi:when test=\"true\">yes</esi:when></esi:choose>more content";
let bytes = Bytes::from_static(input);
let result = parse_complete(&bytes);
match result {
Ok((remaining, elements)) => {
let remaining_str = std::str::from_utf8(remaining).unwrap_or("");
assert!(
remaining_str.starts_with("</esi:choose>"),
"parse_complete should stop before closing tag, but remaining is: {:?}",
remaining_str
);
assert!(!elements.is_empty(), "Should parse at least one element");
}
Err(e) => {
panic!("parse_complete unexpectedly failed: {:?}", e);
}
}
}
#[test]
fn test_why_it_works_parse_fails_early() {
let input = b"<esi:when test=\"true\">content</esi:when></esi:choose>";
let bytes = Bytes::from_static(input);
let streaming_result = parse(&bytes);
match streaming_result {
Ok((remaining, _elements)) => {
let remaining_str = std::str::from_utf8(remaining).unwrap_or("");
assert!(
remaining_str.starts_with("</esi:choose>"),
"Streaming parser should leave closing tag unparsed, but remaining is: {:?}",
remaining_str
);
}
Err(nom::Err::Incomplete(_)) => {
}
Err(e) => {
panic!("Streaming parser unexpectedly failed with error: {:?}", e);
}
}
}
#[test]
fn test_the_magic_sequence() {
use nom::bytes::streaming::tag;
use nom::Parser;
let input = b"<esi:choose><esi:when test=\"true\">yes</esi:whe";
let step1 = tag::<_, _, nom::error::Error<&[u8]>>(&b"<esi:choose>"[..]).parse(input);
let (after_open, _) = step1.expect("Opening tag should succeed");
let bytes2 = Bytes::copy_from_slice(after_open);
let step2 = parse(&bytes2);
assert!(
matches!(step2, Err(nom::Err::Incomplete(_))),
"Expected Incomplete from streaming parse on incomplete <esi:when> tag, got: {:?}",
step2
);
}
#[test]
fn test_parse_complete_on_actually_complete_input() {
let input = b"<esi:include src=\"/test\"/>";
let bytes = Bytes::from_static(input);
let result = parse_complete(&bytes);
match result {
Ok((remaining, elements)) => {
assert!(
remaining.is_empty(),
"Complete input should be fully consumed, but {} bytes remain",
remaining.len()
);
assert!(
!elements.is_empty(),
"Should have parsed at least one element"
);
}
Err(e) => {
panic!("Should parse complete input successfully: {:?}", e);
}
}
}
#[test]
fn test_streaming_incremental_parsing() {
let chunk1 = b"<esi:choose>";
let bytes1 = Bytes::from_static(chunk1);
let result1 = parse(&bytes1);
assert!(
matches!(result1, Err(nom::Err::Incomplete(_))),
"Opening tag only should return Incomplete"
);
let chunk2 = b"<esi:choose>\n <esi:when test=\"true\">";
let bytes2 = Bytes::from_static(chunk2);
let result2 = parse(&bytes2);
assert!(
matches!(result2, Err(nom::Err::Incomplete(_))),
"Incomplete when tag should return Incomplete"
);
let chunk3 = b"<esi:choose>\n <esi:when test=\"true\">content</esi:when>\n</esi:choose>";
let bytes3 = Bytes::from_static(chunk3);
let result3 = parse(&bytes3);
match result3 {
Ok((remaining, elements)) => {
assert_eq!(remaining, b"", "Complete input should be fully consumed");
assert!(!elements.is_empty(), "Should have parsed elements");
}
Err(nom::Err::Incomplete(_)) => {
}
Err(e) => {
panic!("Complete input failed with error: {:?}", e);
}
}
}
#[test]
fn test_theory_parse_complete_used_for_delimited_content() {
use nom::bytes::streaming::tag;
use nom::sequence::delimited;
use nom::Parser;
let input: &[u8] = b"<esi:choose><esi:when test=\"true\">yes</esi:when></esi:choose>";
let result: nom::IResult<&[u8], &[u8], nom::error::Error<&[u8]>> = delimited(
tag(&b"<esi:choose>"[..]),
tag(&b"<esi:when test=\"true\">yes</esi:when>"[..]), tag(&b"</esi:choose>"[..]),
)
.parse(input);
match result {
Ok((remaining, _content)) => {
assert_eq!(remaining, &b""[..], "Should consume entire input");
println!("✓ delimited correctly parses complete content");
}
Err(e) => {
panic!("delimited failed on complete content: {:?}", e);
}
}
}
#[test]
fn test_incomplete_vs_error() {
let incomplete = b"<esi:assign name=\"x\" value=\"";
let bytes1 = Bytes::from_static(incomplete);
let result = parse(&bytes1);
assert!(
matches!(result, Err(nom::Err::Incomplete(_))),
"Incomplete input should return Incomplete, got: {:?}",
result
);
let invalid = b"<esi:invalid:tag:name>";
let bytes2 = Bytes::from_static(invalid);
let result2 = parse(&bytes2);
assert!(
matches!(
result2,
Ok(_) | Err(nom::Err::Error(_)) | Err(nom::Err::Incomplete(_))
),
"Invalid ESI syntax should be handled gracefully"
);
}
#[test]
fn test_all_incomplete_tag_cutoff_positions() {
let test_cases = vec![
("<", "Just opening bracket"),
("<e", "Partial tag name 'e'"),
("<esi", "Partial tag name 'esi'"),
("<esi:", "Tag name with colon"),
("<esi:inc", "Partial 'include'"),
("<esi:include", "Complete tag name, no closing"),
("<esi:include ", "After tag name with space"),
("<esi:include s", "Partial attribute name"),
("<esi:include src", "Complete attribute name, no ="),
("<esi:include src=", "After equals, no quote"),
("<esi:include src=\"", "After opening quote"),
("<esi:include src=\"/path", "Partial attribute value"),
(
"<esi:include src=\"/path/to/file",
"Complete value, no closing quote",
),
(
"<esi:include src=\"/path/to/file\"",
"After closing quote, no >",
),
("<esi:include src=\"/file\"/", "Self-closing, no >"),
("<esi:choose></", "Closing tag start"),
("<esi:choose></e", "Partial closing tag name"),
("<esi:choose></esi:", "Closing tag with colon"),
("<esi:choose></esi:cho", "Partial closing tag 'choose'"),
(
"<esi:choose></esi:choose",
"Complete closing tag name, no >",
),
("<esi:assign", "Assign tag incomplete"),
("<esi:assign name", "Assign with partial attribute"),
("<esi:assign name=", "Assign with attribute name and ="),
("<esi:assign name=\"", "Assign with attribute value started"),
(
"<esi:assign name=\"x\"",
"Assign with one attribute, no closing",
),
(
"<esi:assign name=\"x\" value",
"Assign with second attribute partial",
),
(
"<esi:assign name=\"x\" value=",
"Assign with second attribute =",
),
(
"<esi:assign name=\"x\" value=\"",
"Assign with second value started",
),
("<esi:when", "When tag incomplete"),
("<esi:when test", "When with attribute name"),
("<esi:when test=", "When with ="),
("<esi:when test=\"", "When with test value started"),
("<esi:when test=\"$(HTTP", "When with partial expression"),
("<esi:try", "Try tag incomplete"),
("<esi:attempt", "Attempt tag incomplete"),
("<esi:except", "Except tag incomplete"),
("<esi:comment", "Comment tag incomplete"),
("<esi:remove", "Remove tag incomplete"),
("<esi:vars", "Vars tag incomplete"),
("$(", "Expression start"),
("$(HTTP", "Partial expression"),
("$(HTTP_", "Partial variable name"),
];
for (input, description) in test_cases {
let bytes = Bytes::copy_from_slice(input.as_bytes());
let result = parse(&bytes);
assert!(
matches!(result, Err(nom::Err::Incomplete(_))),
"Test case '{}' ({}): Expected Incomplete, got: {:?}",
input,
description,
result
);
}
}
#[test]
fn test_incomplete_nested_tags() {
let test_cases = vec![
("<esi:choose><esi:when", "Choose with partial when tag"),
("<esi:choose><esi:when test", "Choose with when missing ="),
(
"<esi:choose><esi:when test=",
"Choose with when missing quote",
),
(
"<esi:choose><esi:when test=\"true",
"Choose with when missing closing quote",
),
(
"<esi:choose><esi:when test=\"true\"",
"Choose with when missing >",
),
(
"<esi:choose><esi:when test=\"true\">",
"Choose with when tag open, no content",
),
(
"<esi:choose><esi:when test=\"true\">content",
"Choose with when content, no closing tag",
),
(
"<esi:choose><esi:when test=\"true\">content</esi:when",
"Choose with when partial closing tag",
),
(
"<esi:choose><esi:when test=\"true\">content</esi:when>",
"Choose with complete when, no otherwise/closing",
),
(
"<esi:choose><esi:when test=\"true\">yes</esi:when><esi:otherwise",
"Choose with otherwise partial",
),
("<esi:try><esi:attempt", "Try with partial attempt"),
(
"<esi:try><esi:attempt>",
"Try with attempt open, no content",
),
(
"<esi:try><esi:attempt>content",
"Try with attempt content, no closing",
),
(
"<esi:try><esi:attempt>content</esi:attempt",
"Try with attempt partial closing",
),
(
"<esi:try><esi:attempt>content</esi:attempt><esi:except",
"Try with except partial",
),
("<esi:comment text", "Comment with partial attribute"),
(
"<esi:comment text=\"",
"Comment with attribute value started",
),
("<esi:remove>", "Remove tag open, no content"),
("<esi:remove>content", "Remove with content, no closing"),
(
"<esi:remove>content</esi:remove",
"Remove with partial closing tag",
),
];
for (input, description) in test_cases {
let bytes = Bytes::copy_from_slice(input.as_bytes());
let result = parse(&bytes);
assert!(
matches!(result, Err(nom::Err::Incomplete(_))),
"Test case '{}' ({}): Expected Incomplete, got: {:?}",
input,
description,
result
);
}
}
#[test]
fn test_incomplete_with_whitespace_and_newlines() {
let test_cases = vec![
("<esi:choose>\n", "Choose with newline, no content"),
("<esi:choose>\n ", "Choose with newline and spaces"),
(
"<esi:choose>\n <esi:when",
"Choose with formatted partial when",
),
(
"<esi:choose>\n <esi:when test=\"true\">\n ",
"Choose with when and content whitespace",
),
];
for (input, description) in test_cases {
let bytes = Bytes::copy_from_slice(input.as_bytes());
let result = parse(&bytes);
assert!(
matches!(result, Err(nom::Err::Incomplete(_))),
"Test case '{}' ({}): Expected Incomplete, got: {:?}",
input,
description,
result
);
}
let whitespace_cases = vec![
(" <esi:include", "Leading whitespace with partial tag"),
("\n\n<esi:assign", "Leading newlines with partial tag"),
];
for (input, description) in whitespace_cases {
let bytes = Bytes::copy_from_slice(input.as_bytes());
let result = parse(&bytes);
match result {
Err(nom::Err::Incomplete(_)) => {
}
Ok((remaining, elements)) => {
assert!(
!elements.is_empty() && !remaining.is_empty(),
"Test case '{}' ({}): If Ok, should have parsed Text and have remaining incomplete tag",
input,
description
);
}
other => {
panic!(
"Test case '{}' ({}): Expected Incomplete or Ok with partial parse, got: {:?}",
input, description, other
);
}
}
}
}
#[test]
fn test_incomplete_html_and_script_tags() {
let test_cases = vec![
("<div", "Partial div tag"),
("<div class", "Div with partial attribute"),
("<div class=", "Div with attribute equals"),
("<div class=\"", "Div with attribute value start"),
("<div class=\"container", "Div with partial attribute value"),
(
"<div class=\"container\"",
"Div with complete attribute, no >",
),
("</div", "Partial closing div tag"),
("</di", "Partial closing tag name"),
("</", "Closing tag start only"),
("<script", "Partial script opening tag"),
("<script>", "Script opening tag, REQUIRES closing"),
(
"<script>console.log('test')",
"Script with content, no closing tag",
),
(
"<script>console.log('test')</script",
"Script with partial closing tag",
),
(
"<script>console.log('test')</scri",
"Script with partial closing tag name",
),
(
"<script>console.log('test')</scr",
"Script with partial closing tag",
),
(
"<script>console.log('test')</sc",
"Script with partial closing tag",
),
(
"<script>console.log('test')</s",
"Script with partial closing tag",
),
(
"<script>console.log('test')<",
"Script with closing tag start",
),
("<script type", "Script with partial attribute"),
("<script type=", "Script with attribute equals"),
("<script type=\"", "Script with attribute value start"),
(
"<script type=\"text/javascript",
"Script with partial attribute value",
),
(
"<script type=\"text/javascript\"",
"Script with complete attribute, no >",
),
(
"<script type=\"text/javascript\">",
"Script with attribute, REQUIRES closing",
),
(
"<script type=\"text/javascript\">code",
"Script with attribute and partial content",
),
("<br", "Partial br tag"),
("<br/", "Self-closing br, no >"),
("<img src", "Img with partial attribute"),
("<img src=\"", "Img with attribute value start"),
(
"<img src=\"/path/to/image.jpg",
"Img with partial attribute value",
),
(
"<img src=\"/path/to/image.jpg\"",
"Img with complete attribute, no >",
),
("<img src=\"/path/to/image.jpg\"/", "Img self-closing, no >"),
];
for (input, description) in test_cases {
let bytes = Bytes::copy_from_slice(input.as_bytes());
let result = parse(&bytes);
assert!(
matches!(result, Err(nom::Err::Incomplete(_))),
"Test case '{}' ({}): Expected Incomplete, got: {:?}",
input,
description,
result
);
}
}
#[test]
fn test_streaming_handles_incomplete_attributes() {
use nom::bytes::streaming::{is_not, tag};
use nom::sequence::delimited;
use nom::Parser;
let input: &[u8] = b"\"incomplete_attribute_value";
let content_only = &input[1..]; let is_not_result = is_not::<_, _, nom::error::Error<&[u8]>>(&b"\""[..]).parse(content_only);
assert!(
matches!(is_not_result, Err(nom::Err::Incomplete(_))),
"is_not() should return Incomplete when it doesn't find the delimiter"
);
let delimited_result: nom::IResult<&[u8], &[u8], nom::error::Error<&[u8]>> =
delimited(tag(&b"\""[..]), is_not(&b"\""[..]), tag(&b"\""[..])).parse(input);
assert!(
matches!(delimited_result, Err(nom::Err::Incomplete(_))),
"delimited() should return Incomplete for missing closing delimiter, got: {:?}",
delimited_result
);
let complete_input: &[u8] = b"\"incomplete_attribute_value\"";
let retry_result: nom::IResult<&[u8], &[u8], nom::error::Error<&[u8]>> =
delimited(tag(&b"\""[..]), is_not(&b"\""[..]), tag(&b"\""[..])).parse(complete_input);
assert!(retry_result.is_ok(), "Should succeed with complete input");
let esi_input: &[u8] = b"<esi:choose>\n <esi:when test=\"";
let esi_bytes = Bytes::copy_from_slice(esi_input);
let esi_result = parse(&esi_bytes);
assert!(
matches!(esi_result, Err(nom::Err::Incomplete(_))),
"ESI parser should return Incomplete for incomplete attribute, got: {:?}",
esi_result
);
}