diff options
author | Matt Mastracci <matthew@mastracci.com> | 2024-02-10 13:22:13 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-10 20:22:13 +0000 |
commit | f5e46c9bf2f50d66a953fa133161fc829cecff06 (patch) | |
tree | 8faf2f5831c1c7b11d842cd9908d141082c869a5 /tests/unit/timers_test.ts | |
parent | d2477f780630a812bfd65e3987b70c0d309385bb (diff) |
chore: move cli/tests/ -> tests/ (#22369)
This looks like a massive PR, but it's only a move from cli/tests ->
tests, and updates of relative paths for files.
This is the first step towards aggregate all of the integration test
files under tests/, which will lead to a set of integration tests that
can run without the CLI binary being built.
While we could leave these tests under `cli`, it would require us to
keep a more complex directory structure for the various test runners. In
addition, we have a lot of complexity to ignore various test files in
the `cli` project itself (cargo publish exclusion rules, autotests =
false, etc).
And finally, the `tests/` folder will eventually house the `test_ffi`,
`test_napi` and other testing code, reducing the size of the root repo
directory.
For easier review, the extremely large and noisy "move" is in the first
commit (with no changes -- just a move), while the remainder of the
changes to actual files is in the second commit.
Diffstat (limited to 'tests/unit/timers_test.ts')
-rw-r--r-- | tests/unit/timers_test.ts | 763 |
1 files changed, 763 insertions, 0 deletions
diff --git a/tests/unit/timers_test.ts b/tests/unit/timers_test.ts new file mode 100644 index 000000000..17b137231 --- /dev/null +++ b/tests/unit/timers_test.ts @@ -0,0 +1,763 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +import { + assert, + assertEquals, + assertNotEquals, + delay, + execCode, + unreachable, +} from "./test_util.ts"; + +Deno.test(async function functionParameterBindingSuccess() { + const { promise, resolve } = Promise.withResolvers<void>(); + let count = 0; + + const nullProto = (newCount: number) => { + count = newCount; + resolve(); + }; + + Reflect.setPrototypeOf(nullProto, null); + + setTimeout(nullProto, 500, 1); + await promise; + // count should be reassigned + assertEquals(count, 1); +}); + +Deno.test(async function stringifyAndEvalNonFunctions() { + // eval can only access global scope + const global = globalThis as unknown as { + globalPromise: ReturnType<typeof Promise.withResolvers<void>>; + globalCount: number; + }; + + global.globalPromise = Promise.withResolvers<void>(); + global.globalCount = 0; + + const notAFunction = + "globalThis.globalCount++; globalThis.globalPromise.resolve();" as unknown as () => + void; + + setTimeout(notAFunction, 500); + + await global.globalPromise.promise; + + // count should be incremented + assertEquals(global.globalCount, 1); + + Reflect.deleteProperty(global, "globalPromise"); + Reflect.deleteProperty(global, "globalCount"); +}); + +Deno.test(async function timeoutSuccess() { + const { promise, resolve } = Promise.withResolvers<void>(); + let count = 0; + setTimeout(() => { + count++; + resolve(); + }, 500); + await promise; + // count should increment + assertEquals(count, 1); +}); + +Deno.test(async function timeoutEvalNoScopeLeak() { + // eval can only access global scope + const global = globalThis as unknown as { + globalPromise: ReturnType<typeof Promise.withResolvers<Error>>; + }; + global.globalPromise = Promise.withResolvers(); + setTimeout( + ` + try { + console.log(core); + globalThis.globalPromise.reject(new Error("Didn't throw.")); + } catch (error) { + globalThis.globalPromise.resolve(error); + }` as unknown as () => void, + 0, + ); + const error = await global.globalPromise.promise; + assertEquals(error.name, "ReferenceError"); + Reflect.deleteProperty(global, "globalPromise"); +}); + +Deno.test(async function evalPrimordial() { + const global = globalThis as unknown as { + globalPromise: ReturnType<typeof Promise.withResolvers<void>>; + }; + global.globalPromise = Promise.withResolvers<void>(); + const originalEval = globalThis.eval; + let wasCalled = false; + globalThis.eval = (argument) => { + wasCalled = true; + return originalEval(argument); + }; + setTimeout( + "globalThis.globalPromise.resolve();" as unknown as () => void, + 0, + ); + await global.globalPromise.promise; + assert(!wasCalled); + Reflect.deleteProperty(global, "globalPromise"); + globalThis.eval = originalEval; +}); + +Deno.test(async function timeoutArgs() { + const { promise, resolve } = Promise.withResolvers<void>(); + const arg = 1; + let capturedArgs: unknown[] = []; + setTimeout( + function () { + capturedArgs = [...arguments]; + resolve(); + }, + 10, + arg, + arg.toString(), + [arg], + ); + await promise; + assertEquals(capturedArgs, [ + arg, + arg.toString(), + [arg], + ]); +}); + +Deno.test(async function timeoutCancelSuccess() { + let count = 0; + const id = setTimeout(() => { + count++; + }, 1); + // Cancelled, count should not increment + clearTimeout(id); + await delay(600); + assertEquals(count, 0); +}); + +Deno.test(async function timeoutCancelMultiple() { + function uncalled(): never { + throw new Error("This function should not be called."); + } + + // Set timers and cancel them in the same order. + const t1 = setTimeout(uncalled, 10); + const t2 = setTimeout(uncalled, 10); + const t3 = setTimeout(uncalled, 10); + clearTimeout(t1); + clearTimeout(t2); + clearTimeout(t3); + + // Set timers and cancel them in reverse order. + const t4 = setTimeout(uncalled, 20); + const t5 = setTimeout(uncalled, 20); + const t6 = setTimeout(uncalled, 20); + clearTimeout(t6); + clearTimeout(t5); + clearTimeout(t4); + + // Sleep until we're certain that the cancelled timers aren't gonna fire. + await delay(50); +}); + +Deno.test(async function timeoutCancelInvalidSilentFail() { + // Expect no panic + const { promise, resolve } = Promise.withResolvers<void>(); + let count = 0; + const id = setTimeout(() => { + count++; + // Should have no effect + clearTimeout(id); + resolve(); + }, 500); + await promise; + assertEquals(count, 1); + + // Should silently fail (no panic) + clearTimeout(2147483647); +}); + +Deno.test(async function intervalSuccess() { + const { promise, resolve } = Promise.withResolvers<void>(); + let count = 0; + const id = setInterval(() => { + count++; + clearInterval(id); + resolve(); + }, 100); + await promise; + // Clear interval + clearInterval(id); + // count should increment twice + assertEquals(count, 1); + // Similar false async leaking alarm. + // Force next round of polling. + await delay(0); +}); + +Deno.test(async function intervalCancelSuccess() { + let count = 0; + const id = setInterval(() => { + count++; + }, 1); + clearInterval(id); + await delay(500); + assertEquals(count, 0); +}); + +Deno.test(async function intervalOrdering() { + const timers: number[] = []; + let timeouts = 0; + function onTimeout() { + ++timeouts; + for (let i = 1; i < timers.length; i++) { + clearTimeout(timers[i]); + } + } + for (let i = 0; i < 10; i++) { + timers[i] = setTimeout(onTimeout, 1); + } + await delay(500); + assertEquals(timeouts, 1); +}); + +Deno.test(function intervalCancelInvalidSilentFail() { + // Should silently fail (no panic) + clearInterval(2147483647); +}); + +Deno.test(async function callbackTakesLongerThanInterval() { + const { promise, resolve } = Promise.withResolvers<void>(); + + let timeEndOfFirstCallback: number | undefined; + const interval = setInterval(() => { + if (timeEndOfFirstCallback === undefined) { + // First callback + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 300); + timeEndOfFirstCallback = Date.now(); + } else { + // Second callback + assert(Date.now() - 100 >= timeEndOfFirstCallback); + clearInterval(interval); + resolve(); + } + }, 100); + + await promise; +}); + +// https://github.com/denoland/deno/issues/11398 +Deno.test(async function clearTimeoutAfterNextTimerIsDue1() { + const { promise, resolve } = Promise.withResolvers<void>(); + + setTimeout(() => { + resolve(); + }, 300); + + const interval = setInterval(() => { + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 400); + // Both the interval and the timeout's due times are now in the past. + clearInterval(interval); + }, 100); + + await promise; +}); + +// https://github.com/denoland/deno/issues/11398 +Deno.test(async function clearTimeoutAfterNextTimerIsDue2() { + const { promise, resolve } = Promise.withResolvers<void>(); + + const timeout1 = setTimeout(unreachable, 100); + + setTimeout(() => { + resolve(); + }, 200); + + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 300); + // Both of the timeouts' due times are now in the past. + clearTimeout(timeout1); + + await promise; +}); + +Deno.test(async function fireCallbackImmediatelyWhenDelayOverMaxValue() { + let count = 0; + setTimeout(() => { + count++; + }, 2 ** 31); + await delay(1); + assertEquals(count, 1); +}); + +Deno.test(async function timeoutCallbackThis() { + const { promise, resolve } = Promise.withResolvers<void>(); + let capturedThis: unknown; + const obj = { + foo() { + capturedThis = this; + resolve(); + }, + }; + setTimeout(obj.foo, 1); + await promise; + assertEquals(capturedThis, window); +}); + +Deno.test(async function timeoutBindThis() { + const thisCheckPassed = [null, undefined, window, globalThis]; + + const thisCheckFailed = [ + 0, + "", + true, + false, + {}, + [], + "foo", + () => {}, + Object.prototype, + ]; + + for (const thisArg of thisCheckPassed) { + const { promise, resolve } = Promise.withResolvers<void>(); + let hasThrown = 0; + try { + setTimeout.call(thisArg, () => resolve(), 1); + hasThrown = 1; + } catch (err) { + if (err instanceof TypeError) { + hasThrown = 2; + } else { + hasThrown = 3; + } + } + await promise; + assertEquals(hasThrown, 1); + } + + for (const thisArg of thisCheckFailed) { + let hasThrown = 0; + try { + setTimeout.call(thisArg, () => {}, 1); + hasThrown = 1; + } catch (err) { + if (err instanceof TypeError) { + hasThrown = 2; + } else { + hasThrown = 3; + } + } + assertEquals(hasThrown, 2); + } +}); + +Deno.test(function clearTimeoutShouldConvertToNumber() { + let called = false; + const obj = { + valueOf(): number { + called = true; + return 1; + }, + }; + clearTimeout((obj as unknown) as number); + assert(called); +}); + +Deno.test(function setTimeoutShouldThrowWithBigint() { + let hasThrown = 0; + try { + setTimeout(() => {}, (1n as unknown) as number); + hasThrown = 1; + } catch (err) { + if (err instanceof TypeError) { + hasThrown = 2; + } else { + hasThrown = 3; + } + } + assertEquals(hasThrown, 2); +}); + +Deno.test(function clearTimeoutShouldThrowWithBigint() { + let hasThrown = 0; + try { + clearTimeout((1n as unknown) as number); + hasThrown = 1; + } catch (err) { + if (err instanceof TypeError) { + hasThrown = 2; + } else { + hasThrown = 3; + } + } + assertEquals(hasThrown, 2); +}); + +Deno.test(function testFunctionName() { + assertEquals(clearTimeout.name, "clearTimeout"); + assertEquals(clearInterval.name, "clearInterval"); +}); + +Deno.test(function testFunctionParamsLength() { + assertEquals(setTimeout.length, 1); + assertEquals(setInterval.length, 1); + assertEquals(clearTimeout.length, 0); + assertEquals(clearInterval.length, 0); +}); + +Deno.test(function clearTimeoutAndClearIntervalNotBeEquals() { + assertNotEquals(clearTimeout, clearInterval); +}); + +Deno.test(async function timerOrdering() { + const array: number[] = []; + const { promise: donePromise, resolve } = Promise.withResolvers<void>(); + + function push(n: number) { + array.push(n); + if (array.length === 6) { + resolve(); + } + } + + setTimeout(() => { + push(1); + setTimeout(() => push(4)); + }, 0); + setTimeout(() => { + push(2); + setTimeout(() => push(5)); + }, 0); + setTimeout(() => { + push(3); + setTimeout(() => push(6)); + }, 0); + + await donePromise; + + assertEquals(array, [1, 2, 3, 4, 5, 6]); +}); + +Deno.test(async function timerBasicMicrotaskOrdering() { + let s = ""; + let count = 0; + const { promise, resolve } = Promise.withResolvers<void>(); + setTimeout(() => { + Promise.resolve().then(() => { + count++; + s += "de"; + if (count === 2) { + resolve(); + } + }); + }); + setTimeout(() => { + count++; + s += "no"; + if (count === 2) { + resolve(); + } + }); + await promise; + assertEquals(s, "deno"); +}); + +Deno.test(async function timerNestedMicrotaskOrdering() { + let s = ""; + const { promise, resolve } = Promise.withResolvers<void>(); + s += "0"; + setTimeout(() => { + s += "4"; + setTimeout(() => (s += "A")); + Promise.resolve() + .then(() => { + setTimeout(() => { + s += "B"; + resolve(); + }); + }) + .then(() => { + s += "5"; + }); + }); + setTimeout(() => (s += "6")); + Promise.resolve().then(() => (s += "2")); + Promise.resolve().then(() => + setTimeout(() => { + s += "7"; + Promise.resolve() + .then(() => (s += "8")) + .then(() => { + s += "9"; + }); + }) + ); + Promise.resolve().then(() => Promise.resolve().then(() => (s += "3"))); + s += "1"; + await promise; + assertEquals(s, "0123456789AB"); +}); + +Deno.test(function testQueueMicrotask() { + assertEquals(typeof queueMicrotask, "function"); +}); + +Deno.test(async function timerIgnoresDateOverride() { + const OriginalDate = Date; + const { promise, resolve, reject } = Promise.withResolvers<void>(); + let hasThrown = 0; + try { + const overrideCalled: () => number = () => { + reject("global Date override used over original Date object"); + return 0; + }; + const DateOverride = () => { + overrideCalled(); + }; + globalThis.Date = DateOverride as DateConstructor; + globalThis.Date.now = overrideCalled; + globalThis.Date.UTC = overrideCalled; + globalThis.Date.parse = overrideCalled; + queueMicrotask(() => { + resolve(); + }); + await promise; + hasThrown = 1; + } catch (err) { + if (typeof err === "string") { + assertEquals(err, "global Date override used over original Date object"); + hasThrown = 2; + } else if (err instanceof TypeError) { + hasThrown = 3; + } else { + hasThrown = 4; + } + } finally { + globalThis.Date = OriginalDate; + } + assertEquals(hasThrown, 1); +}); + +Deno.test({ + name: "unrefTimer", + permissions: { run: true, read: true }, + fn: async () => { + const [statusCode, output] = await execCode(` + const timer = setTimeout(() => console.log("1"), 1); + Deno.unrefTimer(timer); + `); + assertEquals(statusCode, 0); + assertEquals(output, ""); + }, +}); + +Deno.test({ + name: "unrefTimer - mix ref and unref 1", + permissions: { run: true, read: true }, + fn: async () => { + const [statusCode, output] = await execCode(` + const timer1 = setTimeout(() => console.log("1"), 200); + const timer2 = setTimeout(() => console.log("2"), 400); + const timer3 = setTimeout(() => console.log("3"), 600); + Deno.unrefTimer(timer3); + `); + assertEquals(statusCode, 0); + assertEquals(output, "1\n2\n"); + }, +}); + +Deno.test({ + name: "unrefTimer - mix ref and unref 2", + permissions: { run: true, read: true }, + fn: async () => { + const [statusCode, output] = await execCode(` + const timer1 = setTimeout(() => console.log("1"), 200); + const timer2 = setTimeout(() => console.log("2"), 400); + const timer3 = setTimeout(() => console.log("3"), 600); + Deno.unrefTimer(timer1); + Deno.unrefTimer(timer2); + `); + assertEquals(statusCode, 0); + assertEquals(output, "1\n2\n3\n"); + }, +}); + +Deno.test({ + name: "unrefTimer - unref interval", + permissions: { run: true, read: true }, + fn: async () => { + const [statusCode, output] = await execCode(` + let i = 0; + const timer1 = setInterval(() => { + console.log("1"); + i++; + if (i === 5) { + Deno.unrefTimer(timer1); + } + }, 10); + `); + assertEquals(statusCode, 0); + assertEquals(output, "1\n1\n1\n1\n1\n"); + }, +}); + +Deno.test({ + name: "unrefTimer - unref then ref 1", + permissions: { run: true, read: true }, + fn: async () => { + const [statusCode, output] = await execCode(` + const timer1 = setTimeout(() => console.log("1"), 10); + Deno.unrefTimer(timer1); + Deno.refTimer(timer1); + `); + assertEquals(statusCode, 0); + assertEquals(output, "1\n"); + }, +}); + +Deno.test({ + name: "unrefTimer - unref then ref", + permissions: { run: true, read: true }, + fn: async () => { + const [statusCode, output] = await execCode(` + const timer1 = setTimeout(() => { + console.log("1"); + Deno.refTimer(timer2); + }, 10); + const timer2 = setTimeout(() => console.log("2"), 20); + Deno.unrefTimer(timer2); + `); + assertEquals(statusCode, 0); + assertEquals(output, "1\n2\n"); + }, +}); + +Deno.test({ + name: "unrefTimer - invalid calls do nothing", + fn: () => { + Deno.unrefTimer(NaN); + Deno.refTimer(NaN); + }, +}); + +Deno.test({ + name: "AbortSignal.timeout() with no listeners", + permissions: { run: true, read: true }, + fn: async () => { + const [statusCode, output] = await execCode(` + const signal = AbortSignal.timeout(2000); + + // This unref timer expires before the signal, and if it does expire, then + // it means the signal has kept the event loop alive. + const timer = setTimeout(() => console.log("Unexpected!"), 1500); + Deno.unrefTimer(timer); + `); + assertEquals(statusCode, 0); + assertEquals(output, ""); + }, +}); + +Deno.test({ + name: "AbortSignal.timeout() with listeners", + permissions: { run: true, read: true }, + fn: async () => { + const [statusCode, output] = await execCode(` + const signal = AbortSignal.timeout(1000); + signal.addEventListener("abort", () => console.log("Event fired!")); + `); + assertEquals(statusCode, 0); + assertEquals(output, "Event fired!\n"); + }, +}); + +Deno.test({ + name: "AbortSignal.timeout() with removed listeners", + permissions: { run: true, read: true }, + fn: async () => { + const [statusCode, output] = await execCode(` + const signal = AbortSignal.timeout(2000); + + const callback = () => console.log("Unexpected: Event fired"); + signal.addEventListener("abort", callback); + + setTimeout(() => { + console.log("Removing the listener."); + signal.removeEventListener("abort", callback); + }, 500); + + Deno.unrefTimer( + setTimeout(() => console.log("Unexpected: Unref timer"), 1500) + ); + `); + assertEquals(statusCode, 0); + assertEquals(output, "Removing the listener.\n"); + }, +}); + +Deno.test({ + name: "AbortSignal.timeout() with listener for a non-abort event", + permissions: { run: true, read: true }, + fn: async () => { + const [statusCode, output] = await execCode(` + const signal = AbortSignal.timeout(2000); + + signal.addEventListener("someOtherEvent", () => { + console.log("Unexpected: someOtherEvent called"); + }); + + Deno.unrefTimer( + setTimeout(() => console.log("Unexpected: Unref timer"), 1500) + ); + `); + assertEquals(statusCode, 0); + assertEquals(output, ""); + }, +}); + +// Regression test for https://github.com/denoland/deno/issues/19866 +Deno.test({ + name: "regression for #19866", + fn: async () => { + const timeoutsFired = []; + + // deno-lint-ignore require-await + async function start(n: number) { + let i = 0; + const intervalId = setInterval(() => { + i++; + if (i > 2) { + clearInterval(intervalId!); + } + timeoutsFired.push(n); + }, 20); + } + + for (let n = 0; n < 100; n++) { + start(n); + } + + // 3s should be plenty of time for all the intervals to fire + // but it might still be flaky on CI. + await new Promise((resolve) => setTimeout(resolve, 3000)); + assertEquals(timeoutsFired.length, 300); + }, +}); + +// Regression test for https://github.com/denoland/deno/issues/20367 +Deno.test({ + name: "regression for #20367", + fn: async () => { + const { promise, resolve } = Promise.withResolvers<number>(); + const start = performance.now(); + setTimeout(() => { + const end = performance.now(); + resolve(end - start); + }, 1000); + clearTimeout(setTimeout(() => {}, 1000)); + + const result = await promise; + assert(result >= 1000); + }, +}); |