use perl_lexer::{MAX_REGEX_PARSE_STEPS, PerlLexer, TokenType};
use std::time::Instant;
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[test]
fn limits_max_regex_parse_steps_is_positive() {
const { assert!(MAX_REGEX_PARSE_STEPS > 0, "MAX_REGEX_PARSE_STEPS must be > 0") };
}
#[test]
fn limits_max_regex_parse_steps_pinned_value() {
assert_eq!(
MAX_REGEX_PARSE_STEPS,
32 * 1024,
"MAX_REGEX_PARSE_STEPS changed unexpectedly; update the doc comment and this pin"
);
}
#[test]
fn limits_step_budget_below_byte_budget() {
const MAX_REGEX_BYTES: usize = 64 * 1024;
const {
assert!(
MAX_REGEX_PARSE_STEPS < MAX_REGEX_BYTES,
"MAX_REGEX_PARSE_STEPS must be < MAX_REGEX_BYTES",
)
};
}
#[test]
fn limits_step_budget_reachable_before_byte_budget() -> TestResult {
let pattern = "a".repeat(MAX_REGEX_PARSE_STEPS + 1_024);
let input = format!("/{pattern}/");
assert!(
input.len() < 64 * 1024,
"Test input ({} bytes) must stay below the byte budget for the step guard to be the first to fire",
input.len()
);
let mut lexer = PerlLexer::new(&input);
let tokens = lexer.collect_tokens();
let found_unknown_rest = tokens.iter().any(|t| matches!(t.token_type, TokenType::UnknownRest));
assert!(
found_unknown_rest,
"Pattern exceeding MAX_REGEX_PARSE_STEPS but under MAX_REGEX_BYTES should emit UnknownRest"
);
Ok(())
}
#[test]
fn limits_q_operator_excessive_nesting_terminates() -> TestResult {
let depth = 200_usize;
let mut input = String::from("q{");
for _ in 0..depth {
input.push('{');
}
input.push_str("hello");
for _ in 0..depth {
input.push('}');
}
input.push('}');
let start = Instant::now();
let mut lexer = PerlLexer::new(&input);
let tokens = lexer.collect_tokens();
let elapsed = start.elapsed();
assert!(elapsed.as_secs() < 2, "Lexer hung on deeply-nested q{{}} (elapsed {:?})", elapsed);
assert!(!tokens.is_empty(), "Lexer must emit at least one token for deeply-nested q{{}}");
Ok(())
}
#[test]
fn limits_qq_operator_excessive_nesting_terminates() -> TestResult {
let depth = 200_usize;
let mut input = String::from("qq{");
for _ in 0..depth {
input.push('{');
}
input.push_str("text");
for _ in 0..depth {
input.push('}');
}
input.push('}');
let start = Instant::now();
let mut lexer = PerlLexer::new(&input);
let tokens = lexer.collect_tokens();
let elapsed = start.elapsed();
assert!(elapsed.as_secs() < 2, "Lexer hung on deeply-nested qq{{}} (elapsed {:?})", elapsed);
assert!(!tokens.is_empty(), "Lexer must emit at least one token for deeply-nested qq{{}}");
Ok(())
}
#[test]
fn limits_m_brace_operator_excessive_nesting_terminates() -> TestResult {
let depth = 200_usize;
let mut input = String::from("m{");
for _ in 0..depth {
input.push('{');
}
input.push('x');
for _ in 0..depth {
input.push('}');
}
input.push('}');
let start = Instant::now();
let mut lexer = PerlLexer::new(&input);
let tokens = lexer.collect_tokens();
let elapsed = start.elapsed();
assert!(elapsed.as_secs() < 2, "Lexer hung on deeply-nested m{{}} (elapsed {:?})", elapsed);
assert!(!tokens.is_empty(), "Lexer must emit at least one token for deeply-nested m{{}}");
Ok(())
}
#[test]
fn limits_heredoc_oversized_body_terminates() -> TestResult {
let line = "some heredoc content line\n";
let repeat_count = (270 * 1024) / line.len() + 1;
let mut input = String::from("my $x = <<END;\n");
for _ in 0..repeat_count {
input.push_str(line);
}
let start = Instant::now();
let mut lexer = PerlLexer::new(&input);
let tokens = lexer.collect_tokens();
let elapsed = start.elapsed();
assert!(elapsed.as_secs() < 2, "Lexer hung on oversized heredoc body (elapsed {:?})", elapsed);
assert!(!tokens.is_empty(), "Lexer must emit at least one token even for an oversized heredoc");
Ok(())
}
#[test]
fn limits_regex_at_step_boundary_parses_successfully() -> TestResult {
let pattern = "a".repeat(MAX_REGEX_PARSE_STEPS - 1);
let input = format!("/{pattern}/");
assert!(
input.len() < 64 * 1024,
"Boundary test must remain below the byte budget ({} bytes)",
input.len()
);
let mut lexer = PerlLexer::new(&input);
let tokens = lexer.collect_tokens();
let has_unknown_rest = tokens.iter().any(|t| matches!(t.token_type, TokenType::UnknownRest));
assert!(
!has_unknown_rest,
"Pattern of exactly MAX_REGEX_PARSE_STEPS-1 chars should not trigger the step budget"
);
let has_regex = tokens.iter().any(|t| matches!(t.token_type, TokenType::RegexMatch));
assert!(
has_regex,
"Pattern just under MAX_REGEX_PARSE_STEPS should produce a RegexMatch token"
);
Ok(())
}