1 #!/usr/bin/env -S ucode -S 2 3 // Debugger Protocol Test Runner (Standalone) 4 // =========================================== 5 // Standalone test runner for the debugger's line-based protocol 6 // (see lib/debug_proto.h) that doesn't rely on cram-style infrastructure. 7 // 8 // Each test starts the target script unmodified via `-X1` (attach mode with 9 // an initial breakpoint at line 1 - the same mechanism the interactive `-x` 10 // CLI and SIGUSR1 attach use), connects to the resulting PID-derived attach 11 // socket directly with the `socket` module, sends a batch of protocol 12 // messages, and asserts on the *parsed* response messages and/or the target 13 // script's own stdout - not on rendered ANSI text, since the server no 14 // longer renders anything. 15 // 16 // `-X1` (rather than `debug.listen(path)`, which drives the whole session 17 // from *inside* a nested native call and needs `uc_vm_resume()` to hand 18 // control back to the VM's bytecode loop) matters here, not just for 19 // realism: breakpoints set *during* a debug.listen() session and hit via a 20 // later CONTINUE don't reliably re-fire through that nested-resume path, 21 // whereas `-X`/`-x`'s breakpoint-loop-driven pause (bk_enter_session invoked 22 // directly from the VM's per-instruction dispatch, see vm.c 23 // uc_vm_decode_insn()) does not have this problem - every test below that 24 // needs a *second* pause after a CONTINUE relies on this. 25 26 import * as fs from 'fs'; 27 import * as sock from 'socket'; 28 29 let testdir = sourcepath(0, true); 30 let topdir = fs.realpath(`${testdir}/..`); 31 let tmpdir = '/tmp/debugger_test.' + system('echo $$'); 32 33 // UCODE_BIN may be a plain executable path, or (as set by the "custom"/ 34 // "debugger" ctest targets) a full command line like 35 // "valgrind --quiet --leak-check=full /path/to/ucode" that needs to be 36 // invoked word-split, not treated as a single path - so fs.dirname(ucode_bin) 37 // would be nonsense for such a value. UCODE_LIB is set alongside it by those 38 // same ctest targets specifically to give the correct library directory 39 // without needing to parse UCODE_BIN at all. 40 let ucode_bin = getenv('UCODE_BIN') || '/home/jow/devel/ucode.git/build/ucode'; 41 let libdir = getenv('UCODE_LIB') || fs.dirname(ucode_bin); 42 43 function shq(s) { 44 return `'${replace(s, "'", "'\\''")}'`; 45 } 46 47 let n_tests = 0; 48 let n_passed = 0; 49 let n_failed = 0; 50 let n_crashed = 0; 51 let n_timeout = 0; 52 53 // Generous enough to tolerate running under valgrind --leak-check=full 54 // (the "custom"/"debugger" ctest targets always do), which can slow 55 // process startup and breakpoint hits down substantially. 56 let TEST_TIMEOUT = 30; 57 58 function mkdir_p(path) { 59 let parts = split(rtrim(path, '/') || '/', /\/+/); 60 let current = ''; 61 for (let part in parts) { 62 current += part + '/'; 63 if (!fs.access(current)) { 64 fs.mkdir(current); 65 } 66 } 67 } 68 69 // Connect to the given Unix domain socket path, retrying for a bit while 70 // the target process is still starting up / hasn't armed its attach socket 71 // yet. 72 function connect_retry(path, timeout_sec) { 73 let deadline = time() + timeout_sec; 74 75 while (time() < deadline) { 76 let conn = sock.connect({ family: sock.AF_UNIX, path }); 77 78 if (conn) { 79 // Bound recv() below so a quiet socket never blocks forever. 80 conn.setopt(sock.SOL_SOCKET, sock.SO_RCVTIMEO, { sec: 0, usec: 20000 }); 81 return conn; 82 } 83 84 sleep(10); 85 } 86 87 return null; 88 } 89 90 // Read every complete "VERB [json]" line already available on `conn` right 91 // now (non-blocking-ish: short poll loop), parsing each into 92 // { verb, payload }. Stops once nothing new arrives for a short quiet 93 // period, since responses may legitimately be a variable-length burst 94 // (e.g. BREAK + BREAKPOINT_ADDED, then a later EVENT exit). 95 function drain_messages(conn, quiet_ms) { 96 let messages = []; 97 let buf = ''; 98 let idle = 0; 99 100 while (idle < quiet_ms) { 101 let chunk = conn.recv(65536); 102 103 if (chunk == null || chunk == '') { 104 idle += 10; 105 sleep(10); 106 continue; 107 } 108 109 idle = 0; 110 buf += chunk; 111 112 let nl; 113 while ((nl = index(buf, "\n")) >= 0) { 114 let line = substr(buf, 0, nl); 115 buf = substr(buf, nl + 1); 116 117 if (line == '') 118 continue; 119 120 let sp = index(line, ' '); 121 let verb = (sp >= 0) ? substr(line, 0, sp) : line; 122 let payload = (sp >= 0) ? json(substr(line, sp + 1)) : null; 123 124 push(messages, { verb, payload }); 125 } 126 } 127 128 return messages; 129 } 130 131 // Run `source_code` unmodified via `-X1` (attach mode, breaking at line 1) 132 // with a batch of protocol messages sent all at once - pacing doesn't 133 // matter since the server processes them strictly in order off its 134 // blocking read loop regardless of when they were written, and these tests 135 // only care about the final observable state (script stdout + which 136 // responses came back), not interactive timing. 137 function run_debugger(source_code, steps, timeout_sec) { 138 if (timeout_sec == null) timeout_sec = TEST_TIMEOUT; 139 mkdir_p(tmpdir); 140 141 let source_file = `${tmpdir}/source.uc`; 142 let stdout_file = `${tmpdir}/stdout.out`; 143 let stderr_file = `${tmpdir}/stderr.err`; 144 let wrapper_file = `${tmpdir}/wrapper.sh`; 145 let pid_file = `${tmpdir}/pid`; 146 147 fs.writefile(source_file, source_code); 148 fs.unlink(pid_file); 149 150 // Runs the target in the background (recording its real PID, *not* 151 // some wrapping shell's) and waits for it, so this test process can 152 // concurrently drive the PID-derived attach socket while the target is 153 // paused at its line-1 breakpoint. Enforces its own timeout by killing 154 // the PID directly rather than via `timeout`, since `timeout` would be 155 // the one owning the PID `$!` reports otherwise. 156 // resolve_breakpoint() can't default the path from a current frame 157 // before the program has started running (there is none yet), so the 158 // pre-execution `-X` breakpoint spec needs an explicit "path:line" 159 // rather than a bare line number. 160 // `ucode_bin` is deliberately left unquoted below: under the "custom"/ 161 // "debugger" ctest targets it is itself a multi-word command 162 // ("valgrind --quiet --leak-check=full /path/ucode") that needs to be 163 // word-split so valgrind sees its own flags, not one opaque argument. 164 fs.writefile(wrapper_file, sprintf( 165 'cd %s\n' + 166 'export LD_LIBRARY_PATH=%s\n' + 167 '%s -L %s -X%s:1 %s > %s 2> %s &\n' + 168 'echo $! > %s\n' + 169 'wait $!\n' + 170 'echo "EXIT:$?"\n', 171 shq(topdir), shq(libdir), ucode_bin, shq(libdir), shq(source_file), shq(source_file), 172 shq(stdout_file), shq(stderr_file), shq(pid_file) 173 )); 174 175 let proc = fs.popen(`sh ${wrapper_file}`, 'r'); 176 let deadline = time() + timeout_sec; 177 let pid = null; 178 179 while (time() < deadline && !pid) { 180 if (fs.access(pid_file)) { 181 let s = trim(fs.readfile(pid_file) ?? ''); 182 if (s != '') 183 pid = int(s); 184 } 185 186 if (!pid) 187 sleep(10); 188 } 189 190 let sock_path = pid ? sprintf('/tmp/ucode-debug-%d.sock', pid) : null; 191 let conn = sock_path ? connect_retry(sock_path, timeout_sec) : null; 192 let messages = []; 193 194 if (conn) { 195 // First message is always the initial PAUSED (from the -X1 196 // breakpoint at line 1). The quiet periods below are tuned short 197 // (150/300ms) to keep the suite fast: the server emits each verb's 198 // response as a tight burst with no deliberate pacing, so a burst 199 // settles well inside that window even under valgrind. If a slow CI 200 // host ever drops a late frame, nudge the second value up. 201 for (let msg in drain_messages(conn, 150)) 202 push(messages, msg); 203 204 let lines = []; 205 for (let step in steps) 206 push(lines, step); 207 // Let anything already paused (including the initial -X1 pause, 208 // for tests with no steps of their own) run to completion first; 209 // QUIT is just a safety net in case something is still paused 210 // afterward (a harmless no-op otherwise, since the connection is 211 // already gone by the time it'd be read). 212 push(lines, 'CONTINUE'); 213 push(lines, 'QUIT'); 214 215 conn.send(join("\n", lines) + "\n"); 216 217 for (let msg in drain_messages(conn, 300)) 218 push(messages, msg); 219 220 conn.close(); 221 } 222 else if (pid) { 223 // Never connected (e.g. no attach socket appeared) - don't leave 224 // the target hanging around forever. 225 system(`kill -9 ${pid} 2>/dev/null`); 226 } 227 228 let wrapper_out = proc.read('all') ?? ''; 229 proc.close(); 230 231 let exitcode = -1; 232 let m = match(wrapper_out, /EXIT:(-?[0-9]+)/); 233 if (m) exitcode = int(m[1]); 234 235 let stdout = fs.access(stdout_file) ? fs.readfile(stdout_file) ?? '' : ''; 236 let stderr = fs.access(stderr_file) ? fs.readfile(stderr_file) ?? '' : ''; 237 238 let timed_out = (exitcode == -1 && !conn); 239 240 return { stdout, stderr, exitcode, timed_out, messages, connected: !!conn }; 241 } 242 243 // True if any received message has the given verb (optionally further 244 // filtered by a predicate over its payload). 245 function has_message(messages, verb, pred) { 246 for (let msg in messages) { 247 if (msg.verb != verb) 248 continue; 249 250 if (!pred || pred(msg.payload)) 251 return true; 252 } 253 254 return false; 255 } 256 257 function run_test(name, source_code, steps, expectations) { 258 n_tests++; 259 260 let result = run_debugger(source_code, steps); 261 let failed = false; 262 let exp = expectations ?? {}; 263 264 if (exp.must_connect && !result.connected) { 265 printf("FAIL %s: could not connect to debug socket\n", name); 266 n_failed++; 267 n_crashed++; 268 return false; 269 } 270 271 if (result.exitcode < 0 && result.exitcode != -1) { 272 if (exp.no_crash) { 273 printf("FAIL %s: crashed (exit code %d)\n", name, result.exitcode); 274 printf(" stderr: %s\n", substr(result.stderr, 0, 200)); 275 n_failed++; 276 n_crashed++; 277 return false; 278 } 279 } 280 281 if (result.timed_out) { 282 if (exp.no_timeout) { 283 printf("FAIL %s: timed out after %ds\n", name, TEST_TIMEOUT); 284 n_failed++; 285 n_timeout++; 286 return false; 287 } 288 } 289 290 if (exp.stdout_contains) { 291 for (let pattern in exp.stdout_contains) { 292 let re = (type(pattern) == 'string') ? regexp(pattern) : pattern; 293 if (!match(result.stdout, re)) { 294 printf("FAIL %s: stdout does not contain '%s'\n", name, pattern); 295 printf(" Got: %s\n", substr(result.stdout, 0, 200)); 296 failed = true; 297 } 298 } 299 } 300 301 if (exp.messages_contain) { 302 for (let verb in exp.messages_contain) { 303 if (!has_message(result.messages, verb)) { 304 printf("FAIL %s: no '%s' response received\n", name, verb); 305 printf(" Got verbs: %s\n", join(', ', map(result.messages, (m) => m.verb))); 306 failed = true; 307 } 308 } 309 } 310 311 if (exp.check) { 312 let msg = exp.check(result); 313 if (msg) { 314 printf("FAIL %s: %s\n", name, msg); 315 failed = true; 316 } 317 } 318 319 if (!failed) { 320 printf("PASS %s\n", name); 321 n_passed++; 322 return true; 323 } 324 325 n_failed++; 326 return false; 327 } 328 329 // ============================================================================ 330 // TEST SUITES 331 // ============================================================================ 332 333 function test_basic_breakpoint() { 334 printf("\n## Basic Breakpoint Tests\n\n"); 335 336 run_test("break_at_line", 337 `print("hello"); 338 print("world"); 339 print("done");`, 340 ['BREAK {"spec":"2"}', 'CONTINUE'], 341 { messages_contain: ['PAUSED', 'BREAKPOINT_ADDED'], no_crash: true } 342 ); 343 344 run_test("break_function", 345 `function test() { 346 print("in test"); 347 } 348 test();`, 349 ['BREAK {"spec":"test"}', 'CONTINUE'], 350 { messages_contain: ['PAUSED', 'BREAKPOINT_ADDED'], no_crash: true } 351 ); 352 353 run_test("break_multiple", 354 `print("a"); 355 print("b"); 356 print("c");`, 357 ['BREAK {"spec":"1"}', 'BREAK {"spec":"2"}', 'LIST_BREAKPOINTS', 'CONTINUE'], 358 { 359 messages_contain: ['BREAKPOINTS'], 360 no_crash: true, 361 check: (r) => { 362 let bp = null; 363 for (let m in r.messages) 364 if (m.verb == 'BREAKPOINTS') bp = m.payload; 365 if (!bp || length(bp.items) < 2) 366 return "expected at least 2 breakpoints listed"; 367 return null; 368 } 369 } 370 ); 371 372 run_test("delete_breakpoint", 373 `print("a"); 374 print("b");`, 375 ['BREAK {"spec":"1"}', 'DELETE {"id":1}', 'LIST_BREAKPOINTS', 'CONTINUE'], 376 { messages_contain: ['OK'], no_crash: true } 377 ); 378 } 379 380 function test_execution_control() { 381 printf("\n## Execution Control Tests\n\n"); 382 383 run_test("step_command", 384 `let x = 1; 385 let y = 2; 386 let z = x + y;`, 387 ['STEP', 'STEP', 'STEP', 'CONTINUE'], 388 { must_connect: true, no_crash: true } 389 ); 390 391 run_test("next_command", 392 `function inner() { return 1; } 393 function outer() { return inner() + 1; } 394 outer();`, 395 ['BREAK {"spec":"outer"}', 'CONTINUE', 'NEXT', 'NEXT', 'CONTINUE'], 396 { must_connect: true, no_crash: true } 397 ); 398 399 run_test("continue_command", 400 `print("a"); 401 print("b"); 402 print("c");`, 403 ['BREAK {"spec":"2"}', 'CONTINUE', 'CONTINUE'], 404 { stdout_contains: ['a', 'b', 'c'], no_crash: true } 405 ); 406 407 run_test("return_command", 408 `function inner() { return 1; } 409 function outer() { return inner() + 1; } 410 outer();`, 411 ['BREAK {"spec":"inner"}', 'CONTINUE', 'RETURN', 'CONTINUE'], 412 { must_connect: true, no_crash: true } 413 ); 414 415 run_test("quit_command", 416 `print("a"); 417 print("b"); 418 print("c");`, 419 [], 420 { no_crash: true } 421 ); 422 } 423 424 function test_variable_inspection() { 425 printf("\n## Variable Inspection Tests\n\n"); 426 427 // STEP (rather than BREAK+CONTINUE to a line number) advances past the 428 // declarations here: resolving a bare line-number breakpoint against a 429 // script that is only variable declarations is a pre-existing, narrow 430 // edge case in resolve_breakpoint()/lookup_stmt_boundary() (unrelated 431 // to the protocol) that single-instruction stepping avoids entirely. 432 run_test("print_simple_var", 433 `let x = 42; 434 let y = "hello"; 435 1;`, 436 ['STEP', 'STEP', 'PRINT {"expr":"x"}', 'PRINT {"expr":"y"}', 'CONTINUE'], 437 { 438 no_crash: true, 439 check: (r) => { 440 let vals = []; 441 for (let m in r.messages) 442 if (m.verb == 'VALUE') push(vals, m.payload.repr); 443 if (!vals[0] || index(vals[0], '42') < 0) return "expected 42 in first VALUE"; 444 if (!vals[1] || index(vals[1], 'hello') < 0) return "expected hello in second VALUE"; 445 return null; 446 } 447 } 448 ); 449 450 run_test("print_expression", 451 `let a = 10; 452 let b = 20; 453 print(a + b);`, 454 ['STEP', 'STEP', 'PRINT {"expr":"a + b"}', 'CONTINUE'], 455 { 456 no_crash: true, 457 check: (r) => { 458 for (let m in r.messages) 459 if (m.verb == 'VALUE' && index(m.payload.repr, '30') >= 0) return null; 460 return "expected VALUE containing 30"; 461 } 462 } 463 ); 464 465 run_test("print_object", 466 `let obj = { foo: "bar", num: 123 }; 467 1;`, 468 ['STEP', 'PRINT {"expr":"obj"}', 'CONTINUE'], 469 { 470 no_crash: true, 471 check: (r) => { 472 for (let m in r.messages) 473 if (m.verb == 'VALUE' && index(m.payload.repr, 'foo') >= 0 && index(m.payload.repr, 'bar') >= 0) 474 return null; 475 return "expected VALUE containing foo/bar"; 476 } 477 } 478 ); 479 480 run_test("print_array", 481 `let arr = [1, 2, 3, 4, 5]; 482 1;`, 483 ['STEP', 'PRINT {"expr":"arr"}', 'CONTINUE'], 484 { 485 no_crash: true, 486 check: (r) => { 487 for (let m in r.messages) 488 if (m.verb == 'VALUE' && index(m.payload.repr, '1') >= 0 && index(m.payload.repr, '3') >= 0) 489 return null; 490 return "expected VALUE containing array elements"; 491 } 492 } 493 ); 494 495 run_test("variables_command", 496 `let x = 1; 497 let y = 2; 498 let z = 3;`, 499 ['VARIABLES'], 500 { messages_contain: ['VARIABLES'], no_crash: true } 501 ); 502 503 run_test("print_nested", 504 `let obj = { nested: { deep: "value" } }; 505 1;`, 506 ['STEP', 'PRINT {"expr":"obj.nested.deep"}', 'CONTINUE'], 507 { 508 no_crash: true, 509 check: (r) => { 510 for (let m in r.messages) 511 if (m.verb == 'VALUE' && index(m.payload.repr, 'value') >= 0) return null; 512 return "expected VALUE containing 'value'"; 513 } 514 } 515 ); 516 } 517 518 function test_stack_tracing() { 519 printf("\n## Stack Tracing Tests\n\n"); 520 521 run_test("backtrace_simple", 522 `function level3() { return 3; } 523 function level2() { return level3(); } 524 function level1() { return level2(); } 525 level1();`, 526 ['BREAK {"spec":"level3"}', 'CONTINUE', 'BACKTRACE {}', 'CONTINUE'], 527 { 528 no_crash: true, 529 check: (r) => { 530 // Tail call optimization reuses the current call frame for the 531 // callee, so the level1/level2 tail-call frames are collapsed 532 // into the level3 frame rather than appearing as separate stack 533 // entries. The backtrace therefore reports the current frame 534 // (level3) plus a tco count of the frames folded into it. 535 for (let m in r.messages) { 536 if (m.verb != 'BACKTRACE') continue; 537 for (let f in m.payload.frames) { 538 if (f.function == 'level3' && f.tco >= 1) 539 return null; 540 } 541 } 542 return "expected level3 frame with a tco count (tail calls collapsed)"; 543 } 544 } 545 ); 546 547 run_test("backtrace_full", 548 `function callee() { return 1; } 549 function caller() { return callee(); } 550 caller();`, 551 ['BREAK {"spec":"callee"}', 'CONTINUE', 'BACKTRACE {"full":true}', 'CONTINUE'], 552 { 553 no_crash: true, 554 check: (r) => { 555 // `caller() { return callee(); }` is a tail call, so the caller 556 // frame is folded into the callee frame by TCO. The full 557 // backtrace reports the current frame (callee) with a tco count 558 // and, because full:true, its local variables. 559 for (let m in r.messages) { 560 if (m.verb != 'BACKTRACE') continue; 561 for (let f in m.payload.frames) { 562 if (f.function == 'callee' && f.tco >= 1 && f.variables) 563 return null; 564 } 565 } 566 return "expected callee frame with tco count and variables"; 567 } 568 } 569 ); 570 571 run_test("bt_alias", 572 `print("test");`, 573 ['BREAK {"spec":"1"}', 'CONTINUE', 'BACKTRACE {}', 'CONTINUE'], 574 { messages_contain: ['BACKTRACE'], no_crash: true } 575 ); 576 } 577 578 function test_source_view() { 579 printf("\n## Source Viewing Tests\n\n"); 580 581 run_test("lines_current", 582 `// line 1 583 // line 2 584 // line 3 585 print("test");`, 586 // A bare, comment-only ":line" spec resolves to the next real 587 // statement (there is no bytecode to break on within a comment), 588 // exactly like -X1's own initial breakpoint already demonstrates. 589 ['BREAK {"spec":"1"}', 'CONTINUE', 'LINES {}', 'CONTINUE'], 590 { messages_contain: ['SOURCE_RANGE'], no_crash: true } 591 ); 592 593 run_test("lines_with_context", 594 `// 1 595 // 2 596 // 3 597 // 4 598 // 5 599 print("test");`, 600 ['BREAK {"spec":"1"}', 'CONTINUE', 'LINES {"before":3,"after":1}', 'CONTINUE'], 601 { messages_contain: ['SOURCE_RANGE'], no_crash: true } 602 ); 603 604 run_test("sources_command", 605 `print("test");`, 606 ['BREAK {"spec":"1"}', 'CONTINUE', 'SOURCES', 'CONTINUE'], 607 { messages_contain: ['SOURCES'], no_crash: true } 608 ); 609 610 run_test("source_fetch", 611 `print("test");`, 612 ['LINES {}'], 613 { 614 no_crash: true, 615 check: (r) => { 616 let file = null; 617 for (let m in r.messages) 618 if (m.verb == 'SOURCE_RANGE') file = m.payload.file; 619 if (!file) return "no SOURCE_RANGE received"; 620 return null; 621 } 622 } 623 ); 624 } 625 626 function test_disassembly() { 627 printf("\n## Disassembly Tests\n\n"); 628 629 run_test("disasm_current", 630 `let x = 1 + 2;`, 631 ['BREAK {"spec":"1"}', 'CONTINUE', 'DISASSEMBLE', 'CONTINUE'], 632 { 633 no_crash: true, 634 check: (r) => { 635 for (let m in r.messages) 636 if (m.verb == 'DISASSEMBLY' && length(m.payload.instructions) > 0) return null; 637 return "expected non-empty DISASSEMBLY"; 638 } 639 } 640 ); 641 642 run_test("disasm_function", 643 `function test() { 644 return 42; 645 } 646 test();`, 647 ['BREAK {"spec":"test"}', 'CONTINUE', 'DISASSEMBLE {"spec":"test"}', 'CONTINUE'], 648 { 649 no_crash: true, 650 check: (r) => { 651 for (let m in r.messages) 652 if (m.verb == 'DISASSEMBLY' && m.payload.function == 'test') return null; 653 return "expected DISASSEMBLY for function 'test'"; 654 } 655 } 656 ); 657 } 658 659 function test_tail_call_optimization() { 660 printf("\n## Tail Call Optimization Tests\n\n"); 661 662 // A tail-recursive function: the compiler emits `I_CALL ... I_RETURN 0x00` 663 // for the tail call, where the trailing 0x00 is a marker byte (never 664 // executed) that flags the call as a tail call. The disassembler must 665 // surface both the call and its terminating return as tail-call-related. 666 run_test("disasm_tail_call", 667 `function count(n) { 668 if (n === 0) 669 return "done"; 670 return count(n - 1); 671 } 672 count(1);`, 673 ['DISASSEMBLE {"spec":"count"}'], 674 { 675 no_crash: true, 676 check: (r) => { 677 for (let m in r.messages) { 678 if (m.verb != 'DISASSEMBLY') continue; 679 let has_call_tail = false, has_return_tail = false; 680 681 for (let ins in m.payload.instructions) { 682 if (ins.call_tail) has_call_tail = true; 683 if (ins.return_tailcall) has_return_tail = true; 684 } 685 686 if (has_call_tail && has_return_tail) return null; 687 return "expected call_tail and return_tailcall in disassembly " + 688 `(got call_tail=${has_call_tail} return_tailcall=${has_return_tail})`; 689 } 690 return "no DISASSEMBLY received"; 691 } 692 } 693 ); 694 695 // The 0x00 marker byte is consumed as part of the I_RETURN that terminates 696 // the tail call, so it must NOT appear as a separate NOOP instruction in 697 // the disassembly. (The compiler never emits I_NOOP anywhere else, so any 698 // NOOP in this chunk would be a spurious marker that failed to consume.) 699 run_test("disasm_tail_call_marker_consumed", 700 `function count(n) { 701 if (n === 0) 702 return "done"; 703 return count(n - 1); 704 } 705 count(1);`, 706 ['DISASSEMBLE {"spec":"count"}'], 707 { 708 no_crash: true, 709 check: (r) => { 710 for (let m in r.messages) { 711 if (m.verb != 'DISASSEMBLY') continue; 712 for (let ins in m.payload.instructions) 713 if (ins.mnemonic == 'NOOP') 714 return "found a spurious NOOP (marker not consumed)"; 715 return null; 716 } 717 return "no DISASSEMBLY received"; 718 } 719 } 720 ); 721 722 // A non-tail call (the result is used in a further expression) must NOT be 723 // flagged as a tail call: there is no 0x00 marker after its I_RETURN. 724 run_test("disasm_non_tail_call", 725 `function non_tail(n) { 726 if (n === 0) 727 return 0; 728 return non_tail(n - 1) + 1; 729 } 730 non_tail(1);`, 731 ['DISASSEMBLE {"spec":"non_tail"}'], 732 { 733 no_crash: true, 734 check: (r) => { 735 for (let m in r.messages) { 736 if (m.verb != 'DISASSEMBLY') continue; 737 for (let ins in m.payload.instructions) { 738 if (ins.call_tail || ins.return_tailcall) 739 return "non-tail call was wrongly flagged as a tail call"; 740 } 741 return null; 742 } 743 return "no DISASSEMBLY received"; 744 } 745 } 746 ); 747 748 // Stepping into a tail-recursive function reuses the current frame for each 749 // tail call, accumulating the count of collapsed frames in frame->tco. The 750 // backtrace must expose that count so the client can show how many frames 751 // were optimized away. 752 run_test("backtrace_tail_call_tco", 753 `function deep(n) { 754 if (n === 0) 755 return "bottom"; 756 return deep(n - 1); 757 } 758 function outer() { 759 return deep(50); 760 } 761 print(outer(), "\n");`, 762 // Step into the recursion; each tail call reuses the frame and bumps tco. 763 ['STEP', 'STEP', 'STEP', 'STEP', 'STEP', 'STEP', 'STEP', 764 'BACKTRACE {}', 'CONTINUE'], 765 { 766 no_crash: true, 767 check: (r) => { 768 for (let m in r.messages) { 769 if (m.verb != 'BACKTRACE') continue; 770 for (let f in m.payload.frames) { 771 if (f.function == 'deep' && f.tco > 0) return null; 772 } 773 } 774 return "expected a backtrace frame with tco > 0"; 775 } 776 } 777 ); 778 } 779 780 function test_eval_sandbox() { 781 printf("\n## Eval Sandbox Tests\n\n"); 782 783 // "return" at the top of an eval sandbox: the expression's outermost frame 784 // has no parent (the call was tail-optimized into the entry frame), so a 785 // normal "return" cannot arm a step breakpoint anywhere. Before the fix 786 // this wedged the client: the inner session ended, the expression ran to 787 // completion, the outer program (still paused) never re-paused, and the 788 // client waited forever for a PAUSED that never came. The server now 789 // synthesizes a PAUSED for the outer frame when the eval sandbox 790 // completes, so the client's prompt comes back and the session continues. 791 run_test("return_in_toplevel_eval", 792 `print("done");`, 793 ['EVAL {"expr":"fn = x => x"}', 794 'BREAK {"spec":"fn"}', 795 'EVAL {"expr":"fn()"}', 796 'RETURN'], 797 { 798 must_connect: true, 799 no_crash: true, 800 no_timeout: true, 801 messages_contain: ['PAUSED', 'BREAKPOINT_ADDED'] 802 } 803 ); 804 805 // A plain eval that hits no breakpoint still completes and re-pauses at 806 // the outer frame (the synthesized PAUSED), leaving the session usable. 807 run_test("eval_no_breakpoint_repauses", 808 `print("done");`, 809 ['EVAL {"expr":"1 + 2"}', 'CONTINUE'], 810 { 811 must_connect: true, 812 no_crash: true, 813 no_timeout: true, 814 messages_contain: ['PAUSED', 'OK'] 815 } 816 ); 817 818 // Stepping off the end of the program ("No next instruction"): the step 819 // cannot resume, so before the fix the server sent a bare ERROR and the 820 // client - having cleared its prompt on sending the resuming STEP - never 821 // got a PAUSED back and wedged with no prompt and no input. The server 822 // now follows the ERROR with a synthesized PAUSED for the still-paused 823 // frame, so the prompt comes back and the session continues to a clean 824 // exit. 825 run_test("step_off_end_repauses", 826 `print("done");`, 827 ['STEP', 'STEP', 'CONTINUE'], 828 { 829 must_connect: true, 830 no_crash: true, 831 no_timeout: true, 832 stdout_contains: ['done'], 833 messages_contain: ['PAUSED'], 834 check: (r) => { 835 // The "No next instruction" error must have been reported. 836 for (let m in r.messages) { 837 if (m.verb != 'ERROR') continue; 838 if (match(m.payload.message ?? '', /No next instruction/)) 839 return null; 840 } 841 return "expected a 'No next instruction' ERROR"; 842 } 843 } 844 ); 845 846 // Nested evals: an eval issued while already paused inside another eval's 847 // sandbox. The eval state save/restore is a plain struct copy into each 848 // eval_expr()'s own C frame (no explicit stack), so this only works 849 // because C's LIFO call-stack order restores each capture in reverse and 850 // the sandbox never frees the buffer the outer frame still points at. Pin 851 // the behavior: define/call fn, then (paused in fn) define/call fn2, then 852 // return out of both - the session must stay coherent and reach main. 853 run_test("nested_eval", 854 `print("done");`, 855 ['EVAL {"expr":"fn = x => x"}', 856 'BREAK {"spec":"fn"}', 857 'EVAL {"expr":"fn()"}', 858 'EVAL {"expr":"fn2 = x => x"}', 859 'BREAK {"spec":"fn2"}', 860 'EVAL {"expr":"fn2()"}', 861 'RETURN', 862 'RETURN'], 863 { 864 must_connect: true, 865 no_crash: true, 866 no_timeout: true, 867 stdout_contains: ['done'], 868 messages_contain: ['PAUSED', 'BREAKPOINT_ADDED'], 869 check: (r) => { 870 // After returning out of both evals we must be back in the 871 // top-level frame, not wedged inside an eval expression. 872 let saw_main = false; 873 for (let m in r.messages) { 874 if (m.verb != 'PAUSED') continue; 875 let f = m.payload.function ?? ''; 876 if (f == 'main') saw_main = true; 877 } 878 return saw_main ? null : "never re-paused in the main frame"; 879 } 880 } 881 ); 882 } 883 884 function test_help_and_misc() { 885 printf("\n## Help and Miscellaneous Tests\n\n"); 886 887 run_test("help_command", 888 `print("test");`, 889 ['BREAK {"spec":"1"}', 'CONTINUE', 'HELP', 'CONTINUE'], 890 { 891 no_crash: true, 892 check: (r) => { 893 for (let m in r.messages) { 894 if (m.verb != 'HELP') continue; 895 let verbs = join(',', map(m.payload.commands, (c) => c.verb)); 896 if (index(verbs, 'BREAK') >= 0 && index(verbs, 'CONTINUE') >= 0 && 897 index(verbs, 'STEP') >= 0 && index(verbs, 'NEXT') >= 0) 898 return null; 899 } 900 return "expected HELP listing BREAK/CONTINUE/STEP/NEXT"; 901 } 902 } 903 ); 904 905 run_test("list_command", 906 `print("a"); 907 print("b");`, 908 ['BREAK {"spec":"1"}', 'BREAK {"spec":"2"}', 'LIST_BREAKPOINTS', 'CONTINUE'], 909 { 910 no_crash: true, 911 check: (r) => { 912 for (let m in r.messages) 913 if (m.verb == 'BREAKPOINTS' && length(m.payload.items) >= 2) return null; 914 return "expected at least 2 listed breakpoints"; 915 } 916 } 917 ); 918 919 run_test("invalid_command", 920 `print("test");`, 921 ['BREAK {"spec":"1"}', 'CONTINUE', 'BOGUS', 'CONTINUE'], 922 { messages_contain: ['ERROR'], no_crash: true } 923 ); 924 } 925 926 function test_debug_api() { 927 printf("\n## Debug API Tests\n\n"); 928 929 run_test("traceback_function", 930 `function level3() { return debug.traceback(); } 931 function level2() { return level3(); } 932 function level1() { return level2(); } 933 let result = level1(); 934 print("done"); 935 print(result);`, 936 [], 937 // level1/level2 are tail calls, so TCO folds their frames into the 938 // level3 frame; debug.traceback() therefore reports level3 (where the 939 // traceback is taken) and main, but not the collapsed intermediate 940 // frames. 941 { stdout_contains: ['done', 'level3', 'main'], no_crash: true } 942 ); 943 944 run_test("sourcepos_function", 945 `function test() { 946 let pos = debug.sourcepos(); 947 print("line", pos.line); 948 } 949 test();`, 950 [], 951 { stdout_contains: ['line'], no_crash: true } 952 ); 953 954 run_test("getinfo_function", 955 `function test() { return 1; } 956 let info = debug.getinfo(test); 957 print("done");`, 958 [], 959 { stdout_contains: ['done'], no_crash: true } 960 ); 961 962 run_test("debugger_api", 963 `function test() { 964 print("inside test"); 965 } 966 test(); 967 print("after");`, 968 ['BREAK {"spec":"test"}', 'CONTINUE', 'CONTINUE'], 969 { stdout_contains: ['inside test', 'after'], no_crash: true } 970 ); 971 } 972 973 function test_edge_cases() { 974 printf("\n## Edge Cases and Bug Tests\n\n"); 975 976 run_test("empty_commands", 977 `print("test");`, 978 [], 979 { no_crash: true } 980 ); 981 982 run_test("rapid_breakpoints", 983 `print("a"); 984 print("b"); 985 print("c"); 986 print("d"); 987 print("e");`, 988 ['BREAK {"spec":"1"}', 'BREAK {"spec":"2"}', 'BREAK {"spec":"3"}', 'BREAK {"spec":"4"}', 989 'BREAK {"spec":"5"}', 'LIST_BREAKPOINTS', 'CONTINUE'], 990 { no_crash: true } 991 ); 992 993 run_test("invalid_breakpoint", 994 `print("test");`, 995 ['BREAK {"spec":"999"}', 'CONTINUE'], 996 { no_crash: true } 997 ); 998 999 run_test("delete_invalid", 1000 `print("test");`, 1001 ['DELETE {"id":999}', 'CONTINUE'], 1002 { messages_contain: ['ERROR'], no_crash: true } 1003 ); 1004 1005 run_test("print_undefined", 1006 `print("test");`, 1007 ['BREAK {"spec":"1"}', 'CONTINUE', 'PRINT {"expr":"undefined_var"}', 'CONTINUE'], 1008 { no_crash: true } 1009 ); 1010 1011 run_test("deep_recursion", 1012 `function recurse(n) { 1013 if (n <= 0) return 0; 1014 return recurse(n - 1) + 1; 1015 } 1016 recurse(100);`, 1017 ['BREAK {"spec":"recurse"}', 'CONTINUE'], 1018 { no_crash: true, no_timeout: true } 1019 ); 1020 1021 run_test("large_object", 1022 `let obj = {}; 1023 for (let i = 0; i < 100; i++) { 1024 obj["key" + i] = i; 1025 }`, 1026 ['BREAK {"spec":"1"}', 'CONTINUE', 'PRINT {"expr":"obj"}', 'CONTINUE'], 1027 { no_crash: true } 1028 ); 1029 1030 run_test("closure_upvalues", 1031 `function makeCounter() { 1032 let count = 0; 1033 return function() { count++; return count; }; 1034 } 1035 let counter = makeCounter(); 1036 counter();`, 1037 ['BREAK {"spec":"counter"}', 'CONTINUE', 'PRINT {"expr":"counter()"}', 'CONTINUE'], 1038 { no_crash: true } 1039 ); 1040 1041 run_test("exception_in_debug", 1042 `try { 1043 die("test error"); 1044 } catch (e) { 1045 print("caught"); 1046 }`, 1047 // One CONTINUE for the initial -X1 pause, one more for the 1048 // automatic BK_CATCH pause the try/catch's die() triggers. 1049 ['CONTINUE'], 1050 { stdout_contains: ['caught'], no_crash: true } 1051 ); 1052 } 1053 1054 function test_memory_safety() { 1055 printf("\n## Memory Safety Tests\n\n"); 1056 1057 run_test("repeated_inspection", 1058 `let x = 1; 1059 let y = 2; 1060 let z = 3;`, 1061 ['BREAK {"spec":"1"}', 'CONTINUE', 'PRINT {"expr":"x"}', 'PRINT {"expr":"y"}', 1062 'PRINT {"expr":"z"}', 'PRINT {"expr":"x"}', 'PRINT {"expr":"y"}', 'CONTINUE'], 1063 { no_crash: true } 1064 ); 1065 1066 run_test("disasm_variants", 1067 `let a = 1; 1068 let b = "str"; 1069 let c = [1, 2, 3]; 1070 let d = { x: 1 }; 1071 function f() { return 1; }`, 1072 ['BREAK {"spec":"1"}', 'CONTINUE', 'DISASSEMBLE', 'DISASSEMBLE {"spec":"f"}', 'CONTINUE'], 1073 { no_crash: true } 1074 ); 1075 1076 run_test("mixed_frames", 1077 `replace("test", "t", function(m) { 1078 return m.toUpperCase(); 1079 });`, 1080 [], 1081 { no_crash: true } 1082 ); 1083 } 1084 1085 // ============================================================================ 1086 // MAIN 1087 // ============================================================================ 1088 1089 printf('\n##\n## Running Debugger Tests\n##\n\n'); 1090 1091 try { 1092 mkdir_p(tmpdir); 1093 1094 test_basic_breakpoint(); 1095 test_execution_control(); 1096 test_variable_inspection(); 1097 test_stack_tracing(); 1098 test_source_view(); 1099 test_disassembly(); 1100 test_tail_call_optimization(); 1101 test_eval_sandbox(); 1102 test_help_and_misc(); 1103 test_debug_api(); 1104 test_edge_cases(); 1105 test_memory_safety(); 1106 } 1107 catch (e) { 1108 warn(`Test runner error: ${e.type}: ${e.message}\n${e.stacktrace[0].context}\n`); 1109 } 1110 1111 // Cleanup 1112 system(['rm', '-rf', tmpdir]); 1113 1114 printf('\n##\n## Test Summary\n##\n\n'); 1115 printf('Ran %d tests: %d passed, %d failed', n_tests, n_passed, n_failed); 1116 if (n_crashed > 0) printf(' (%d crashes)', n_crashed); 1117 if (n_timeout > 0) printf(' (%d timeouts)', n_timeout); 1118 printf('\n'); 1119 1120 exit(n_failed > 0 ? 1 : 0);
This page was automatically generated by LXR 0.3.1. • OpenWrt