1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-09 17:28:00 +00:00

Add fuzz testing and parser edge case regression tests

Fuzz targets (cargo +nightly fuzz):
- fuzz_lexer: lexer must never panic on arbitrary input
- fuzz_parser: parser must never panic on arbitrary input
- fuzz_compile: full pipeline (parse → analyze → IR → optimize →
  codegen → assemble → link) must never panic

Results: 3.3M iterations across all targets, zero crashes.
Seed corpus from all 7 examples + crafted edge cases.

11 new parser edge case tests:
- draw_followed_by_statement: regression for keyword-arg lookahead bug
- draw_with_frame_followed_by_statement: same with frame: arg
- nested_if_else_chain: 3-deep else-if chain
- deeply_nested_blocks: 4-deep nested if blocks
- empty_function_body: fun noop() {}
- empty_state_handlers: on enter/exit/frame {}
- button_start_in_condition: "start" keyword as button name
- button_select_in_condition: "select" keyword as button name
- multiple_draws_in_sequence: 3 draws followed by assignment
- parser_no_panic_on_garbage: 25 malformed inputs
- lexer_no_panic_on_garbage: 10 malformed inputs

221 tests total, all pre-commit checks pass.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
Claude 2026-04-12 00:52:35 +00:00
parent d7092f703d
commit 07dd5bd7e8
No known key found for this signature in database
8 changed files with 781 additions and 0 deletions

View file

@ -678,3 +678,240 @@ fn parse_bank_decl() {
assert_eq!(prog.banks[0].name, "Level1Data");
assert_eq!(prog.banks[0].bank_type, BankType::Prg);
}
// ── Edge cases and regression tests ──
#[test]
fn draw_followed_by_statement() {
// Regression: draw keyword-arg parser used to consume the next statement's
// identifier as a keyword arg, causing "expected ':'" errors.
let src = r#"
game "T" { mapper: NROM }
var i: u8 = 0
on frame {
draw Sprite at: (0, 0)
i += 1
}
start Main
"#;
let prog = parse_ok(src);
let frame = prog.states[0].on_frame.as_ref().unwrap();
assert_eq!(frame.statements.len(), 2); // draw + assignment
}
#[test]
fn draw_with_frame_followed_by_statement() {
let src = r#"
game "T" { mapper: NROM }
var x: u8 = 0
on frame {
draw Sprite at: (0, 0) frame: 1
x = 5
}
start Main
"#;
let prog = parse_ok(src);
let frame = prog.states[0].on_frame.as_ref().unwrap();
assert_eq!(frame.statements.len(), 2);
}
#[test]
fn nested_if_else_chain() {
let src = r#"
game "T" { mapper: NROM }
var x: u8 = 0
on frame {
if x == 0 {
x = 1
} else if x == 1 {
x = 2
} else if x == 2 {
x = 3
} else {
x = 0
}
}
start Main
"#;
let prog = parse_ok(src);
let frame = prog.states[0].on_frame.as_ref().unwrap();
match &frame.statements[0] {
Statement::If(_, _, else_ifs, else_block, _) => {
assert_eq!(else_ifs.len(), 2);
assert!(else_block.is_some());
}
other => panic!("expected If, got {other:?}"),
}
}
#[test]
fn deeply_nested_blocks() {
let src = r#"
game "T" { mapper: NROM }
var x: u8 = 0
on frame {
if x == 0 {
if x == 0 {
if x == 0 {
if x == 0 {
x = 1
}
}
}
}
}
start Main
"#;
parse_ok(src); // should not stack overflow
}
#[test]
fn empty_function_body() {
let src = r#"
game "T" { mapper: NROM }
fun noop() {}
on frame { wait_frame }
start Main
"#;
let prog = parse_ok(src);
assert_eq!(prog.functions.len(), 1);
assert!(prog.functions[0].body.statements.is_empty());
}
#[test]
fn empty_state_handlers() {
let src = r#"
game "T" { mapper: NROM }
state Main {
on enter {}
on exit {}
on frame {}
}
start Main
"#;
let prog = parse_ok(src);
assert!(prog.states[0]
.on_enter
.as_ref()
.unwrap()
.statements
.is_empty());
assert!(prog.states[0]
.on_exit
.as_ref()
.unwrap()
.statements
.is_empty());
assert!(prog.states[0]
.on_frame
.as_ref()
.unwrap()
.statements
.is_empty());
}
#[test]
fn button_start_in_condition() {
// "start" is a keyword but valid as a button name
let src = r#"
game "T" { mapper: NROM }
on frame {
if button.start { wait_frame }
}
start Main
"#;
let prog = parse_ok(src);
let frame = prog.states[0].on_frame.as_ref().unwrap();
match &frame.statements[0] {
Statement::If(Expr::ButtonRead(_, button, _), _, _, _, _) => {
assert_eq!(button, "start");
}
other => panic!("expected if with button.start, got {other:?}"),
}
}
#[test]
fn button_select_in_condition() {
let src = r#"
game "T" { mapper: NROM }
on frame {
if button.select { wait_frame }
}
start Main
"#;
parse_ok(src); // "select" is a keyword too — must parse as button name
}
#[test]
fn multiple_draws_in_sequence() {
let src = r#"
game "T" { mapper: NROM }
var x: u8 = 0
on frame {
draw A at: (0, 0)
draw B at: (10, 10)
draw C at: (20, 20)
x += 1
}
start Main
"#;
let prog = parse_ok(src);
let frame = prog.states[0].on_frame.as_ref().unwrap();
assert_eq!(frame.statements.len(), 4); // 3 draws + 1 assignment
}
#[test]
fn parser_no_panic_on_garbage() {
// The parser should never panic, only return errors.
let garbage_inputs = [
"",
"{}{}{}",
"game",
"game \"\"",
"game \"\" {",
"game \"\" { mapper: }",
"var : = 0",
"fun () {}",
"if if if",
"while { }",
"draw",
"draw X",
"draw X at:",
"draw X at: (",
"draw X at: (0",
"draw X at: (0,",
"draw X at: (0, 0",
"sprite {",
"palette { colors: }",
"[[[",
"))))",
"0x 0b",
"\"unterminated",
"/* no block comments */",
"!@#$%",
&"x ".repeat(1000), // long input
];
for input in &garbage_inputs {
let _ = parse(input); // must not panic
}
}
#[test]
fn lexer_no_panic_on_garbage() {
use crate::lexer::lex;
let garbage_inputs = [
"\0\0\0",
"\x01\x02\x03",
"\"\\",
"0x",
"0b",
"99999999999999999999",
"0xFFFFFFFFFF",
&"a".repeat(10000),
"!!!!!!",
"\t\r\n \t\r\n",
];
for input in &garbage_inputs {
let _ = lex(input); // must not panic
}
}