From 51d19833896d9d84ea736ec16c05992cf25d4c8f Mon Sep 17 00:00:00 2001 From: theMackabu Date: Tue, 27 Jan 2026 16:01:18 -0800 Subject: [PATCH] add coroutine/async this preservation tests --- src/ant.c | 8 +- src/modules/process.c | 58 +++++++++++ tests/test_coro_this.js | 219 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 280 insertions(+), 5 deletions(-) create mode 100644 tests/test_coro_this.js diff --git a/src/ant.c b/src/ant.c index c01cbcd..1fbe2c3 100644 --- a/src/ant.c +++ b/src/ant.c @@ -11140,13 +11140,11 @@ static jsval_t js_try(struct js *js) { js->pos = try_start; js->consumed = 1; + mkscope(js); while (next(js) != TOK_EOF && next(js) != TOK_RBRACE && !(js->flags & (F_RETURN | F_THROW | F_BREAK))) { try_result = js_stmt(js); - if (is_err(try_result)) { - had_exception = true; - break; - } - } + if (is_err(try_result)) { had_exception = true; break; } + } delscope(js); if (js->flags & F_RETURN) { try_returned = true; diff --git a/src/modules/process.c b/src/modules/process.c index 9c98308..e04e695 100644 --- a/src/modules/process.c +++ b/src/modules/process.c @@ -309,6 +309,23 @@ static void emit_stdio_event(ProcessEventType *events, const char *event_type, j } } +static bool remove_listener_from_events(ProcessEventType *events, const char *event, jsval_t listener) { + ProcessEventType *evt = NULL; + HASH_FIND_STR(events, event, evt); + if (!evt) return false; + + for (int i = 0; i < evt->listener_count; i++) { + if (evt->listeners[i].listener != listener) continue; + memmove( + &evt->listeners[i], &evt->listeners[i + 1], + (size_t)(evt->listener_count - i - 1) * sizeof(ProcessEventListener) + ); + return --evt->listener_count == 0; + } + + return false; +} + static bool stdin_is_tty(void) { return uv_guess_handle(STDIN_FILENO) == UV_TTY; } @@ -501,6 +518,19 @@ static jsval_t js_stdin_remove_all_listeners(ant_t *js, jsval_t *args, int nargs return this_obj; } +static jsval_t js_stdin_remove_listener(ant_t *js, jsval_t *args, int nargs) { + jsval_t this_obj = js_getthis(js); + if (nargs < 2) return this_obj; + + char *event = js_getstr(js, args[0], NULL); + if (!event) return this_obj; + + bool now_empty = remove_listener_from_events(stdin_events, event, args[1]); + if (now_empty && strcmp(event, "data") == 0) stdin_stop_reading(); + + return this_obj; +} + static jsval_t js_stdout_write(ant_t *js, jsval_t *args, int nargs) { if (nargs < 1) return js_mkfalse(); size_t len = 0; @@ -570,6 +600,17 @@ static jsval_t js_stdout_remove_all_listeners(ant_t *js, jsval_t *args, int narg return this_obj; } +static jsval_t js_stdout_remove_listener(ant_t *js, jsval_t *args, int nargs) { + jsval_t this_obj = js_getthis(js); + if (nargs < 2) return this_obj; + + char *event = js_getstr(js, args[0], NULL); + if (!event) return this_obj; + + remove_listener_from_events(stdout_events, event, args[1]); + return this_obj; +} + static jsval_t js_stdout_get_window_size(ant_t *js, jsval_t *args, int nargs) { (void)args; (void)nargs; int rows = 0, cols = 0; @@ -673,6 +714,17 @@ static jsval_t js_stderr_remove_all_listeners(ant_t *js, jsval_t *args, int narg return this_obj; } +static jsval_t js_stderr_remove_listener(ant_t *js, jsval_t *args, int nargs) { + jsval_t this_obj = js_getthis(js); + if (nargs < 2) return this_obj; + + char *event = js_getstr(js, args[0], NULL); + if (!event) return this_obj; + + remove_listener_from_events(stderr_events, event, args[1]); + return this_obj; +} + static jsval_t process_uptime(ant_t *js, jsval_t *args, int nargs) { (void)args; (void)nargs; uint64_t now = uv_hrtime(); @@ -1376,6 +1428,8 @@ void init_process_module() { js_set(js, stdin_proto, "resume", js_mkfun(js_stdin_resume)); js_set(js, stdin_proto, "pause", js_mkfun(js_stdin_pause)); js_set(js, stdin_proto, "on", js_mkfun(js_stdin_on)); + js_set(js, stdin_proto, "removeListener", js_mkfun(js_stdin_remove_listener)); + js_set(js, stdin_proto, "off", js_mkfun(js_stdin_remove_listener)); js_set(js, stdin_proto, "removeAllListeners", js_mkfun(js_stdin_remove_all_listeners)); js_set(js, stdin_proto, get_toStringTag_sym_key(), js_mkstr(js, "ReadStream", 10)); @@ -1388,6 +1442,8 @@ void init_process_module() { js_set(js, stdout_proto, "write", js_mkfun(js_stdout_write)); js_set(js, stdout_proto, "on", js_mkfun(js_stdout_on)); js_set(js, stdout_proto, "once", js_mkfun(js_stdout_once)); + js_set(js, stdout_proto, "removeListener", js_mkfun(js_stdout_remove_listener)); + js_set(js, stdout_proto, "off", js_mkfun(js_stdout_remove_listener)); js_set(js, stdout_proto, "removeAllListeners", js_mkfun(js_stdout_remove_all_listeners)); js_set(js, stdout_proto, "getWindowSize", js_mkfun(js_stdout_get_window_size)); js_set(js, stdout_proto, get_toStringTag_sym_key(), js_mkstr(js, "WriteStream", 11)); @@ -1403,6 +1459,8 @@ void init_process_module() { js_set(js, stderr_proto, "write", js_mkfun(js_stderr_write)); js_set(js, stderr_proto, "on", js_mkfun(js_stderr_on)); js_set(js, stderr_proto, "once", js_mkfun(js_stderr_once)); + js_set(js, stderr_proto, "removeListener", js_mkfun(js_stderr_remove_listener)); + js_set(js, stderr_proto, "off", js_mkfun(js_stderr_remove_listener)); js_set(js, stderr_proto, "removeAllListeners", js_mkfun(js_stderr_remove_all_listeners)); js_set(js, stderr_proto, get_toStringTag_sym_key(), js_mkstr(js, "WriteStream", 11)); diff --git a/tests/test_coro_this.js b/tests/test_coro_this.js new file mode 100644 index 0000000..2ab0e0a --- /dev/null +++ b/tests/test_coro_this.js @@ -0,0 +1,219 @@ +// Test that `this` is preserved correctly in coroutine/async contexts + +class Widget { + constructor(name) { + this.name = name; + this._value = 42; + this._boundMethod = this._boundMethod.bind(this); + } + + _boundMethod() { + return this._value; + } + + unboundMethod() { + return this._value; + } + + callBound() { + return this._boundMethod(); + } + + callUnbound() { + return this.unboundMethod(); + } +} + +let passed = 0; +let failed = 0; + +function test(name, fn) { + try { + const result = fn(); + if (result instanceof Promise) { + result.then(() => { + console.log(`✓ ${name}`); + passed++; + }).catch(e => { + console.log(`✗ ${name}`); + console.log(` Error: ${e.message}`); + failed++; + }); + } else { + console.log(`✓ ${name}`); + passed++; + } + } catch (e) { + console.log(`✗ ${name}`); + console.log(` Error: ${e.message}`); + failed++; + } +} + +function assertEqual(a, b, msg) { + if (a !== b) throw new Error(msg || `Expected ${b}, got ${a}`); +} + +console.log('=== Coroutine/Async this preservation tests ===\n'); + +// Sync tests +console.log('-- Synchronous --'); + +test('Direct method call preserves this', () => { + const w = new Widget('test'); + assertEqual(w.callBound(), 42); +}); + +test('Bound method works', () => { + const w = new Widget('test'); + assertEqual(w._boundMethod(), 42); +}); + +test('Unbound method via this works', () => { + const w = new Widget('test'); + assertEqual(w.callUnbound(), 42); +}); + +// Test in callback context +console.log('\n-- In callback context --'); + +test('Method call in setTimeout callback', () => { + return new Promise((resolve, reject) => { + const w = new Widget('test'); + setTimeout(() => { + try { + assertEqual(w.callBound(), 42); + resolve(); + } catch (e) { + reject(e); + } + }, 10); + }); +}); + +test('Method call in Promise.then', () => { + const w = new Widget('test'); + return Promise.resolve().then(() => { + assertEqual(w.callBound(), 42); + }); +}); + +// Test in event handler context (simulated) +console.log('\n-- In event handler context --'); + +test('Method call from stored handler', () => { + const w = new Widget('test'); + const handlers = []; + + handlers.push(() => { + return w.callBound(); + }); + + for (const h of handlers) { + assertEqual(h(), 42); + } +}); + +test('Bound method passed as callback', () => { + const w = new Widget('test'); + const fn = w._boundMethod; + assertEqual(fn(), 42); +}); + +// Test class with stdin-like event pattern +console.log('\n-- Event emitter pattern --'); + +class FakeScreen { + constructor() { + this._running = true; + this._handlers = []; + this._cleanup = this._cleanup.bind(this); + } + + _cleanup() { + if (!this._running) return 'already stopped'; + this._running = false; + return 'cleaned up'; + } + + onKey(fn) { + this._handlers.push(fn); + } + + exit() { + const result = this._cleanup(); + return result; + } + + emitKey(key) { + for (const h of this._handlers) { + h(key); + } + } +} + +test('Exit method calls _cleanup correctly', () => { + const screen = new FakeScreen(); + let exitResult = null; + + screen.onKey((key) => { + if (key === 'q') { + exitResult = screen.exit(); + } + }); + + screen.emitKey('q'); + assertEqual(exitResult, 'cleaned up'); +}); + +// Test with async handler +console.log('\n-- Async event handlers --'); + +test('Exit in async handler', async () => { + const screen = new FakeScreen(); + let exitResult = null; + + screen.onKey(async (key) => { + if (key === 'q') { + await Promise.resolve(); // simulate async work + exitResult = screen.exit(); + } + }); + + screen.emitKey('q'); + await Promise.resolve(); + await Promise.resolve(); // Give time for async handler + assertEqual(exitResult, 'cleaned up'); +}); + +// Test stdin data event pattern +console.log('\n-- stdin.on pattern --'); + +test('Method call from stdin data handler', () => { + return new Promise((resolve, reject) => { + const screen = new FakeScreen(); + let result = null; + + const handler = (chunk) => { + const str = chunk.toString ? chunk.toString() : chunk; + if (str === 'q') { + try { + result = screen.exit(); + assertEqual(result, 'cleaned up'); + resolve(); + } catch (e) { + reject(e); + } + } + }; + + // Simulate stdin event + setTimeout(() => handler('q'), 10); + }); +}); + +// Wait for async tests +setTimeout(() => { + console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`); + process.exit(failed > 0 ? 1 : 0); +}, 200); -- 2.51.2