import { gc } from "bun"; import { describe, test, expect, mock, beforeEach } from "bun:test"; import { channel, Channel, hasSubscribers, subscribe, unsubscribe } from "node:diagnostics_channel"; import { AsyncLocalStorage } from "node:async_hooks"; describe("Channel", () => { // test-diagnostics-channel-has-subscribers.js test("can have subscribers", () => { const name = "channel1"; const dc = channel(name); expect(hasSubscribers(name)).toBeFalse(); dc.subscribe(() => {}); expect(hasSubscribers(name)).toBeTrue(); checkCalls(); }); // test-diagnostics-channel-symbol-named.js test("can have symbol as name", () => { const input = { foo: "bar", }; const symbol = Symbol("channel2"); // Individual channel objects can be created to avoid future lookups const dc = channel(symbol); // Expect two successful publishes later dc.subscribe( mustCall((message, name) => { expect(name).toBe(symbol); expect(message).toStrictEqual(input); }), ); dc.publish(input); expect(() => { // @ts-expect-error channel(null); }).toThrow(/channel argument must be of type/); checkCalls(); }); // test-diagnostics-channel-sync-unsubscribe.js test("does not throw when unsubscribed", () => { const name = "channel3"; const data = "some message"; const onMessageHandler: any = mustCall(() => unsubscribe(name, onMessageHandler)); subscribe(name, onMessageHandler); // This must not throw. channel(name).publish(data); checkCalls(); }); // test-diagnostics-channel-pub-sub.js test("can publish and subscribe", () => { const name = "channel4"; const input = { foo: "bar", }; // Individual channel objects can be created to avoid future lookups const dc = channel(name); expect(dc).toBeInstanceOf(Channel); // No subscribers yet, should not publish expect(dc.hasSubscribers).toBeFalse(); const subscriber = mustCall((message, name) => { expect(name).toBe(dc.name); expect(message).toStrictEqual(input); }); // Now there's a subscriber, should publish subscribe(name, subscriber); expect(dc.hasSubscribers).toBeTrue(); // The ActiveChannel prototype swap should not fail instanceof expect(dc).toBeInstanceOf(Channel); // Should trigger the subscriber once dc.publish(input); // Should not publish after subscriber is unsubscribed expect(unsubscribe(name, subscriber)).toBeTrue(); expect(dc.hasSubscribers).toBeFalse(); // unsubscribe() should return false when subscriber is not found expect(unsubscribe(name, subscriber)).toBeFalse(); expect(() => { // @ts-expect-error subscribe(name, null); }).toThrow(/subscription argument must be of type/); // Reaching zero subscribers should not delete from the channels map as there // will be no more weakref to incRef if another subscribe happens while the // channel object itself exists. dc.subscribe(subscriber); dc.unsubscribe(subscriber); dc.subscribe(subscriber); checkCalls(); }); // test-diagnostics-channel-object-channel-pub-sub.js test("can publish and subscribe using object", () => { const name = "channel5"; const input = { foo: "bar", }; // Should not have named channel expect(hasSubscribers(name)).toBeFalse(); // Individual channel objects can be created to avoid future lookups const dc = channel(name); expect(dc).toBeInstanceOf(Channel); expect(channel(name)).toBe(dc); // intentional object equality check // No subscribers yet, should not publish expect(dc.hasSubscribers).toBeFalse(); const subscriber = mustCall((message, name) => { expect(name).toBe(dc.name); expect(message).toStrictEqual(input); }); // Now there's a subscriber, should publish dc.subscribe(subscriber); expect(dc.hasSubscribers).toBeTrue(); // The ActiveChannel prototype swap should not fail instanceof expect(dc).toBeInstanceOf(Channel); // Should trigger the subscriber once dc.publish(input); // Should not publish after subscriber is unsubscribed expect(dc.unsubscribe(subscriber)).toBeTrue(); expect(dc.hasSubscribers).toBeFalse(); // unsubscribe() should return false when subscriber is not found expect(dc.unsubscribe(subscriber)).toBeFalse(); expect(() => { // @ts-expect-error subscribe(null); }).toThrow(/channel argument must be of type/); checkCalls(); }); // test-diagnostics-channel-safe-subscriber-errors.js // TODO: Needs support for 'uncaughtException' event test.todo("can handle subscriber errors", () => { const input = { foo: "bar", }; const dc = channel("channel6"); const error = new Error("This error should have been caught!"); process.on( "uncaughtException", mustCall(err => { expect(err).toStrictEqual(error); }), ); dc.subscribe( mustCall(() => { throw error; }), ); // The failing subscriber should not stop subsequent subscribers from running dc.subscribe(mustCall(() => {})); // Publish should continue without throwing const fn = mustCall(() => {}); dc.publish(input); fn(); checkCalls(); }); // test-diagnostics-channel-bind-store.js // TODO: Needs support for 'uncaughtException' event test.todo("can use bind store", () => { let n = 0; const name = "channel7"; const thisArg = new Date(); const inputs = [{ foo: "bar" }, { baz: "buz" }]; const dc = channel(name); // Bind a storage directly to published data const store1 = new AsyncLocalStorage(); dc.bindStore(store1); let store1bound = true; // Bind a store with transformation of published data const store2 = new AsyncLocalStorage(); dc.bindStore( store2, mustCall(data => { expect(data).toStrictEqual(inputs[n]); return { data }; }, 4), ); // Regular subscribers should see publishes from runStores calls dc.subscribe( mustCall(data => { if (store1bound) { expect(data).toStrictEqual(store1.getStore()); } expect({ data }).toStrictEqual(store2.getStore()); expect(data).toStrictEqual(inputs[n]); }, 4), ); // Verify stores are empty before run expect(store1.getStore()).toBeUndefined(); expect(store2.getStore()).toBeUndefined(); dc.runStores( inputs[n], mustCall(function (a, b) { // Verify this and argument forwarding expect(this).toBe(thisArg); expect(a).toBe(1); expect(b).toBe(2); // Verify store 1 state matches input expect(store1.getStore()).toStrictEqual(inputs[n]); // Verify store 2 state has expected transformation expect(store2.getStore()).toStrictEqual({ data: inputs[n] }); // Should support nested contexts n++; dc.runStores( inputs[n], mustCall(function () { // Verify this and argument forwarding expect(this).toBeUndefined(); // Verify store 1 state matches input expect(store1.getStore()).toStrictEqual(inputs[n]); // Verify store 2 state has expected transformation expect(store2.getStore()).toStrictEqual({ data: inputs[n] }); }), ); n--; // Verify store 1 state matches input expect(store1.getStore()).toStrictEqual(inputs[n]); // Verify store 2 state has expected transformation expect(store2.getStore()).toStrictEqual({ data: inputs[n] }); }), thisArg, 1, 2, ); // Verify stores are empty after run expect(store1.getStore()).toBeUndefined(); expect(store2.getStore()).toBeUndefined(); // Verify unbinding works expect(dc.unbindStore(store1)).toBeTrue(); store1bound = false; // Verify unbinding a store that is not bound returns false expect(dc.unbindStore(store1)).toBeFalse(); n++; dc.runStores( inputs[n], mustCall(() => { // Verify after unbinding store 1 will remain undefined expect(store1.getStore()).toBeUndefined(); // Verify still bound store 2 receives expected data expect(store2.getStore()).toStrictEqual({ data: inputs[n] }); }), ); // Contain transformer errors and emit on next tick const fail = new Error("fail"); dc.bindStore(store1, () => { throw fail; }); let calledRunStores = false; process.once( "uncaughtException", mustCall(err => { expect(calledRunStores).toBeTrue(); expect(err).toStrictEqual(fail); }), ); dc.runStores( inputs[n], mustCall(() => {}), ); calledRunStores = true; checkCalls(); }); // test-diagnostics-channel-memory-leak.js test("references are not leaked", () => { function noop() {} const heapUsedBefore = process.memoryUsage().heapUsed; for (let i = 0; i < 1000; i++) { const name = `channel7-${i}`; subscribe(name, noop); unsubscribe(name, noop); } gc(true); const heapUsedAfter = process.memoryUsage().heapUsed; expect(heapUsedBefore).toBeGreaterThanOrEqual(heapUsedAfter); }); }); describe("TracingChannel", () => { // Port tests from: // https://github.com/search?q=repo%3Anodejs%2Fnode+test-diagnostics-channel+AND+%2Ftracing%2F&type=code test.todo("TODO"); }); const mocks = new Map(); function mustCall(fn: (...args: any[]) => T, expected?: number) { const instance = mock(fn); mocks.set(instance, expected ?? 1); return instance; } function mustNotCall(fn: (...args: any[]) => T) { return mustCall(fn, 0); } // FIXME: remove this and use `afterEach` instead // Currently, `bun test` disallows `expect()` in `afterEach` function checkCalls() { for (const [mock, expected] of mocks.entries()) { expect(mock).toHaveBeenCalledTimes(expected); } mocks.clear(); } beforeEach(() => { mocks.clear(); });