mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
372 lines
10 KiB
TypeScript
372 lines
10 KiB
TypeScript
import { gc } from "bun";
|
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
import { channel, Channel, hasSubscribers, subscribe, unsubscribe } from "node:diagnostics_channel";
|
|
|
|
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 string or symbol/);
|
|
|
|
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<T>(fn: (...args: any[]) => T, expected?: number) {
|
|
const instance = mock(fn);
|
|
mocks.set(instance, expected ?? 1);
|
|
return instance;
|
|
}
|
|
|
|
function mustNotCall<T>(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();
|
|
});
|