test(pdftract-5upi): add acceptance criteria tests for structural token lexer

Add comprehensive tests for array/dict delimiters, keywords, indirect
references, stream header validation, and edge cases like case-mismatched
keywords.

All tests verify the existing lexer implementation handles:
- [1 2 3] -> ArrayStart, Integer(1), Integer(2), Integer(3), ArrayEnd
- << /A 1 >> -> DictStart, Name(b"A"), Integer(1), DictEnd
- <48> -> String(b"\x48") (NOT dict - < vs << distinction)
- <<<48>>> -> DictStart, String(b"\x48"), DictEnd
- true false null -> Bool(true), Bool(false), Null
- 12 0 obj null endobj -> Integer(12), Integer(0), Obj, Null, EndObj
- 5 0 R -> Integer(5), Integer(0), IndirectRef
- stream\n vs stream\r -> StructInvalidStreamHeader for lone CR
- True (case-mismatched) -> Token::Keyword(b"True")
- proptest: random bytes never panic, always terminate with Eof

Addresses pdftract-5upi acceptance criteria.
This commit is contained in:
jedarden 2026-05-20 18:52:35 -04:00
parent deb79bba9c
commit 60ae7ea561

View file

@ -2009,4 +2009,88 @@ mod tests {
}
});
}
// Acceptance criteria tests for pdftract-5upi
#[test]
fn array_with_integers() {
// Acceptance: [1 2 3] -> ArrayStart, Integer(1), Integer(2), Integer(3), ArrayEnd, Eof
let mut lexer = Lexer::new(b"[1 2 3]");
assert_eq!(lexer.next_token(), Some(Token::ArrayStart));
assert_eq!(lexer.next_token(), Some(Token::Integer(1)));
assert_eq!(lexer.next_token(), Some(Token::Integer(2)));
assert_eq!(lexer.next_token(), Some(Token::Integer(3)));
assert_eq!(lexer.next_token(), Some(Token::ArrayEnd));
assert_eq!(lexer.next_token(), Some(Token::Eof));
}
#[test]
fn dict_with_name_and_integer() {
// Acceptance: << /A 1 >> -> DictStart, Name(b"A"), Integer(1), DictEnd, Eof
let mut lexer = Lexer::new(b"<< /A 1 >>");
assert_eq!(lexer.next_token(), Some(Token::DictStart));
assert_eq!(lexer.next_token(), Some(Token::Name(b"A".to_vec())));
assert_eq!(lexer.next_token(), Some(Token::Integer(1)));
assert_eq!(lexer.next_token(), Some(Token::DictEnd));
assert_eq!(lexer.next_token(), Some(Token::Eof));
}
#[test]
fn indirect_object_header_and_null() {
// Acceptance: 12 0 obj null endobj -> Integer(12), Integer(0), Obj, Null, EndObj, Eof
let mut lexer = Lexer::new(b"12 0 obj null endobj");
assert_eq!(lexer.next_token(), Some(Token::Integer(12)));
assert_eq!(lexer.next_token(), Some(Token::Integer(0)));
assert_eq!(lexer.next_token(), Some(Token::Obj));
assert_eq!(lexer.next_token(), Some(Token::Null));
assert_eq!(lexer.next_token(), Some(Token::EndObj));
assert_eq!(lexer.next_token(), Some(Token::Eof));
}
#[test]
fn indirect_reference_pattern() {
// Acceptance: 5 0 R -> Integer(5), Integer(0), IndirectRef, Eof
let mut lexer = Lexer::new(b"5 0 R");
assert_eq!(lexer.next_token(), Some(Token::Integer(5)));
assert_eq!(lexer.next_token(), Some(Token::Integer(0)));
assert_eq!(lexer.next_token(), Some(Token::IndirectRef));
assert_eq!(lexer.next_token(), Some(Token::Eof));
}
#[test]
fn bool_and_null_sequence() {
// Acceptance: true false null -> Bool(true), Bool(false), Null, Eof
let mut lexer = Lexer::new(b"true false null");
assert_eq!(lexer.next_token(), Some(Token::Bool(true)));
assert_eq!(lexer.next_token(), Some(Token::Bool(false)));
assert_eq!(lexer.next_token(), Some(Token::Null));
assert_eq!(lexer.next_token(), Some(Token::Eof));
}
#[test]
fn proptest_random_bytes_never_panics() {
use proptest::prelude::*;
// Generate random byte sequences and verify they never panic
let test_strategy = prop::collection::vec(prop::num::u8::ANY, 0..1000);
proptest!(|(bytes in test_strategy)| {
// This should never panic
let mut lexer = Lexer::new(&bytes);
// Consume all tokens until Eof
let mut token_count = 0;
let max_tokens = 10000; // Safety limit to prevent infinite loops
while token_count < max_tokens {
match lexer.next_token() {
Some(Token::Eof) | None => break,
Some(_) => token_count += 1,
}
}
// Should always terminate with Eof
prop_assert!(token_count < max_tokens, "Token stream did not terminate");
});
}
}