Skip to content

Commit

Permalink
Disallow Cancelable value in delay(msec, value) (#84)
Browse files Browse the repository at this point in the history
* Disallow Cancelable value in delay(msec, value)

* Add jest.config.js to npmignore
  • Loading branch information
rbuckton committed Jul 19, 2023
1 parent 7973b87 commit c57fa71
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 104 deletions.
59 changes: 12 additions & 47 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,12 @@
module.exports = {
projects: [
'<rootDir>/internal/collections-hash',
'<rootDir>/internal/ts-worker',
'<rootDir>/packages/async-autoresetevent/jest.config.js',
'<rootDir>/packages/async-barrier/jest.config.js',
'<rootDir>/packages/async-conditionvariable/jest.config.js',
'<rootDir>/packages/async-countdown/jest.config.js',
'<rootDir>/packages/async-deferred/jest.config.js',
'<rootDir>/packages/async-iter-fn/jest.config.js',
'<rootDir>/packages/async-iter-fromsync/jest.config.js',
'<rootDir>/packages/async-iter-query/jest.config.js',
'<rootDir>/packages/async-manualresetevent/jest.config.js',
'<rootDir>/packages/async-mutex/jest.config.js',
'<rootDir>/packages/async-queue/jest.config.js',
'<rootDir>/packages/async-readerwriterlock/jest.config.js',
'<rootDir>/packages/async-semaphore/jest.config.js',
'<rootDir>/packages/async-stack/jest.config.js',
'<rootDir>/packages/canceltoken/jest.config.js',
'<rootDir>/packages/collection-core/jest.config.js',
'<rootDir>/packages/collection-core-shim/jest.config.js',
'<rootDir>/packages/collections-hashmap/jest.config.js',
'<rootDir>/packages/collections-hashset/jest.config.js',
'<rootDir>/packages/collections-multimap/jest.config.js',
'<rootDir>/packages/collections-sortedmap/jest.config.js',
'<rootDir>/packages/collections-sortedset/jest.config.js',
'<rootDir>/packages/decorators/jest.config.js',
'<rootDir>/packages/disposable/jest.config.js',
'<rootDir>/packages/equatable/jest.config.js',
'<rootDir>/packages/equatable-shim/jest.config.js',
'<rootDir>/packages/fn/jest.config.js',
'<rootDir>/packages/fn-partial/jest.config.js',
'<rootDir>/packages/indexed-object/jest.config.js',
'<rootDir>/packages/iter-fn/jest.config.js',
'<rootDir>/packages/iter-lookup/jest.config.js',
'<rootDir>/packages/iter-query/jest.config.js',
'<rootDir>/packages/lazy/jest.config.js',
'<rootDir>/packages/ref/jest.config.js',
'<rootDir>/packages/reflect-metadata-compat/jest.config.js',
'<rootDir>/packages/struct-type/jest.config.js',
'<rootDir>/packages/threading-autoresetevent/jest.config.js',
'<rootDir>/packages/threading-conditionvariable/jest.config.js',
'<rootDir>/packages/threading-manualresetevent/jest.config.js',
'<rootDir>/packages/threading-mutex/jest.config.js',
'<rootDir>/packages/threading-sleep/jest.config.js'
],
};
const fs = require("node:fs");
const path = require("node:path");
const projects = [];
for (const workspaceRoot of ["internal", "packages"]) {
for (const entry of fs.readdirSync(workspaceRoot, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
if (fs.existsSync(path.join(workspaceRoot, entry.name, "jest.config.js"))) {
projects.push(`<rootDir>/${workspaceRoot}/${entry.name}/jest.config.js`);
}
}
}
module.exports = { projects };
58 changes: 12 additions & 46 deletions jest.esm.config.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,12 @@
module.exports = {
projects: [
'<rootDir>/internal/collections-hash/jest.esm.config.js',
'<rootDir>/packages/async-autoresetevent/jest.esm.config.js',
'<rootDir>/packages/async-barrier/jest.esm.config.js',
'<rootDir>/packages/async-conditionvariable/jest.esm.config.js',
'<rootDir>/packages/async-countdown/jest.esm.config.js',
'<rootDir>/packages/async-deferred/jest.esm.config.js',
'<rootDir>/packages/async-iter-fn/jest.esm.config.js',
'<rootDir>/packages/async-iter-fromsync/jest.esm.config.js',
'<rootDir>/packages/async-iter-query/jest.esm.config.js',
'<rootDir>/packages/async-manualresetevent/jest.esm.config.js',
'<rootDir>/packages/async-mutex/jest.esm.config.js',
'<rootDir>/packages/async-queue/jest.esm.config.js',
'<rootDir>/packages/async-readerwriterlock/jest.esm.config.js',
'<rootDir>/packages/async-semaphore/jest.esm.config.js',
'<rootDir>/packages/async-stack/jest.esm.config.js',
'<rootDir>/packages/canceltoken/jest.esm.config.js',
'<rootDir>/packages/collection-core/jest.esm.config.js',
'<rootDir>/packages/collection-core-shim/jest.esm.config.js',
'<rootDir>/packages/collections-hashmap/jest.esm.config.js',
'<rootDir>/packages/collections-hashset/jest.esm.config.js',
'<rootDir>/packages/collections-multimap/jest.esm.config.js',
'<rootDir>/packages/collections-sortedmap/jest.esm.config.js',
'<rootDir>/packages/collections-sortedset/jest.esm.config.js',
'<rootDir>/packages/decorators/jest.esm.config.js',
'<rootDir>/packages/disposable/jest.esm.config.js',
'<rootDir>/packages/equatable/jest.esm.config.js',
'<rootDir>/packages/equatable-shim/jest.esm.config.js',
'<rootDir>/packages/fn/jest.esm.config.js',
'<rootDir>/packages/fn-partial/jest.esm.config.js',
'<rootDir>/packages/indexed-object/jest.esm.config.js',
'<rootDir>/packages/iter-fn/jest.esm.config.js',
'<rootDir>/packages/iter-lookup/jest.esm.config.js',
'<rootDir>/packages/iter-query/jest.esm.config.js',
'<rootDir>/packages/lazy/jest.esm.config.js',
'<rootDir>/packages/ref/jest.esm.config.js',
'<rootDir>/packages/reflect-metadata-compat/jest.esm.config.js',
'<rootDir>/packages/struct-type/jest.esm.config.js',
'<rootDir>/packages/threading-autoresetevent/jest.esm.config.js',
'<rootDir>/packages/threading-conditionvariable/jest.esm.config.js',
'<rootDir>/packages/threading-manualresetevent/jest.esm.config.js',
'<rootDir>/packages/threading-mutex/jest.esm.config.js',
'<rootDir>/packages/threading-sleep/jest.esm.config.js',
],
};
const fs = require("node:fs");
const path = require("node:path");
const projects = [];
for (const workspaceRoot of ["internal", "packages"]) {
for (const entry of fs.readdirSync(workspaceRoot, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
if (fs.existsSync(path.join(workspaceRoot, entry.name, "jest.esm.config.js"))) {
projects.push(`<rootDir>/${workspaceRoot}/${entry.name}/jest.esm.config.js`);
}
}
}
module.exports = { projects };
3 changes: 2 additions & 1 deletion packages/async-delay/.npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ api-extractor.json
tsconfig*.json
*.tsbuildinfo
.npmfiles
*.tgz
*.tgz
/jest*.config.js
5 changes: 5 additions & 0 deletions packages/async-delay/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
...require("../../jest.base.config"),
displayName: "async-delay",
roots: ["<rootDir>"]
};
4 changes: 4 additions & 0 deletions packages/async-delay/jest.esm.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
...require("../../jest.esm.base.config"),
displayName: `${require("./jest.config.js").displayName}`,
};
3 changes: 3 additions & 0 deletions packages/async-delay/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
"dependencies": {
"@esfx/cancelable": "workspace:*"
},
"devDependencies": {
"@esfx/internal-guards": "workspace:*"
},
"publishConfig": {
"access": "public"
},
Expand Down
183 changes: 183 additions & 0 deletions packages/async-delay/src/__tests__/delay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { Cancelable, CancelableCancelSignal, CancelError, CancelSubscription } from "@esfx/cancelable";
import { delay } from "..";

const waitOne = () => new Promise(res => jest.requireActual("timers").setImmediate(res));

jest.useFakeTimers();
describe("delay(msec)", () => {
it("rejects if msec not number", async () => {
expect.assertions(1);
await expect(delay({} as any)).rejects.toThrow(TypeError);
});
it("rejects if msec is negative", async () => {
expect.assertions(1);
await expect(delay(-1)).rejects.toThrow(TypeError);
});
it("rejects if msec is Infinity", async () => {
expect.assertions(1);
await expect(delay(Infinity)).rejects.toThrow(TypeError);
});
it("rejects if msec is NaN", async () => {
expect.assertions(1);
await expect(delay(NaN)).rejects.toThrow(TypeError);
});
it("does not resolve synchronously when msec = 0", async () => {
expect.assertions(2);

let delayPromiseResolved = false;
const delayPromise = delay(0).then(() => { delayPromiseResolved = true });
expect(delayPromiseResolved).toBe(false); // not resolved synchronously

jest.advanceTimersByTime(1);
await waitOne();
expect(delayPromiseResolved).toBe(true); // resolved immediately

await delayPromise;
});
it("resolves after timeout", async () => {
expect.assertions(3);

let delayPromiseResolved = false;
const delayPromise = delay(100).then(() => { delayPromiseResolved = true });
expect(delayPromiseResolved).toBe(false); // not resolved synchronously

await waitOne();
expect(delayPromiseResolved).toBe(false); // not resolved immediately

jest.advanceTimersByTime(100);

await delayPromise;
expect(delayPromiseResolved).toBe(true); // resolved after timeout
});
it("resolves to undefined", async () => {
expect.assertions(1);
const delayPromise = delay(100);
jest.advanceTimersByTime(100);
await expect(delayPromise).resolves.toBeUndefined();
});
});
describe("delay(msec, value)", () => {
it("rejects if value is Canceable", async () => {
const cancelable: Cancelable = { [Cancelable.cancelSignal]: jest.fn() };
expect.assertions(1);
await expect(delay(0, cancelable as unknown)).rejects.toThrow(TypeError);
});
it("resolves to value when non-promise", async () => {
expect.assertions(1);
const delayPromise = delay(100, 1);
jest.advanceTimersByTime(100);
await expect(delayPromise).resolves.toBe(1);
});
it("resolves to awaited value when promise", async () => {
expect.assertions(1);
const delayPromise = delay(100, Promise.resolve(1));
jest.advanceTimersByTime(100);
await expect(delayPromise).resolves.toBe(1);
});
it("rejects if promise value rejects", async () => {
expect.assertions(1);
const delayPromise = delay(100, Promise.reject("rejected"));
jest.advanceTimersByTime(100);
await expect(delayPromise).rejects.toBe("rejected");
});
});
describe("delay(cancelable, msec)", () => {
it("rejects if cancelable is canceled", async () => {
expect.assertions(1);
const cancelable: CancelableCancelSignal = {
[Cancelable.cancelSignal]() { return this; },
signaled: true,
reason: new CancelError(),
subscribe: jest.fn()
};
const delayPromise = delay(cancelable, 0);
jest.advanceTimersByTime(0);
await expect(delayPromise).rejects.toBe(cancelable.reason);
});
it("rejects if cancelable is non-null, non-undefined, non-cancelable", async () => {
expect.assertions(1);
const delayPromise = delay({} as any, 0);
jest.advanceTimersByTime(0);
await expect(delayPromise).rejects.toThrow(TypeError);
});
it("cancelable can be null", async () => {
expect.assertions(1);
const delayPromise = delay(null, 0);
jest.advanceTimersByTime(0);
await expect(delayPromise).resolves.toBeUndefined();
});
it("cancelable can be undefined", async () => {
expect.assertions(1);
const delayPromise = delay(undefined, 0);
jest.advanceTimersByTime(0);
await expect(delayPromise).resolves.toBeUndefined();
});
it("cancelable can Cancelable", async () => {
expect.assertions(1);
const subscription = CancelSubscription.create(jest.fn());
const cancelable: CancelableCancelSignal = {
[Cancelable.cancelSignal]() { return this; },
signaled: false,
reason: undefined,
subscribe: jest.fn().mockReturnValue(subscription)
};
const delayPromise = delay(cancelable, 0);
jest.advanceTimersByTime(0);
await expect(delayPromise).resolves.toBeUndefined();
});
it("subscribes to Cancelable", async () => {
expect.assertions(1);
const subscription = CancelSubscription.create(jest.fn());
const cancelable: CancelableCancelSignal = {
[Cancelable.cancelSignal]() { return this; },
signaled: false,
reason: undefined,
subscribe: jest.fn().mockReturnValue(subscription)
};
delay(cancelable, 0);
expect(cancelable.subscribe).toHaveBeenCalled();
});
it("unsubscribes from Cancelable subscription on resolve", async () => {
expect.assertions(1);
const unsubscribe = jest.fn();
const subscription = CancelSubscription.create(unsubscribe);
const cancelable: CancelableCancelSignal = {
[Cancelable.cancelSignal]() { return this; },
signaled: false,
reason: undefined,
subscribe: jest.fn().mockReturnValue(subscription)
};
const delayPromise = delay(cancelable, 0);
jest.advanceTimersByTime(0);
await delayPromise;
expect(unsubscribe).toHaveBeenCalled();
});
it("rejects if Cancelable is signaled after start", async () => {
expect.assertions(1);
let onSignaled!: () => void;
let signaled = false;
let reason: unknown = undefined;
const unsubscribe = jest.fn();
const subscription = CancelSubscription.create(unsubscribe);
const cancelable: CancelableCancelSignal = {
[Cancelable.cancelSignal]() { return this; },
get signaled() { return signaled; },
get reason() { return reason; },
subscribe: (cb) => (onSignaled = cb, subscription),
};
const delayPromise = delay(cancelable, 0);
signaled = true;
reason = new CancelError();
onSignaled();
await expect(delayPromise).rejects.toThrow(CancelError);
});
});
describe("delay(cancelable, msec, value)", () => {
it("does not reject if value is Canceable", async () => {
expect.assertions(1);
const cancelable: Cancelable = { [Cancelable.cancelSignal]: jest.fn() };
const delayPromise = delay(undefined, 0, cancelable);
jest.advanceTimersByTime(0);
await expect(delayPromise).resolves.toBe(cancelable);
});
});
Loading

0 comments on commit c57fa71

Please sign in to comment.