-
Notifications
You must be signed in to change notification settings - Fork 5.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Deno.test() should support timeout option #11133
Comments
We had a PR for this quite a while back (#7372). |
Landed in std denoland/std#1022 import { deadline } from "https://deno.land/std/async/mod.ts";
Deno.test("footer", deadline(async function() {
}, 400); |
I still think this should be built-in option to test definition instead of requiring to import it from |
If we do it from Rust, so that even blocking operations time out then sure sgtm. |
That sounds good - we could discover code blocking forever - at first glance it looks like we'd need 3 separate threads for that - main ("coordinator") thread, timeout thread and thread running test code. Any ideas for implementation? |
Yeah I already tried this in the refactor that I punted, should be even cleaner to do that now with |
Hi everyone, I've started working on this to learn more about the testing part of the codebase and would like to discuss my current approach (which can't detect timeouts due to blocking operations) and how to implement the timeout handling in Rust. The current problemCurrently, if a test's promise never resolves, Deno will not execute any of the other tests within that file. Imagine, for example, that you had the following file (a simplification of #13146): // empty_event_loop_test.ts
function willResolve(shouldResolve: boolean): Promise<void> {
return new Promise((resolve) => {
if (shouldResolve) {
resolve();
}
});
}
Deno.test("runs to completion", async () => {
await willResolve(true);
});
Deno.test("never resolves", async () => {
await willResolve(false);
});
Deno.test("never executed", () => {}); If you try to
Deno never executes the third test because the second test will cause a promise to be pending, but the stack and the event-loop will be empty, causing the Lines 703 to 712 in 340764a
As shown above, this error is thrown because the runtime polls My current solution (detecting non-blocking timeouts)My current approach can trigger timeouts that happen due to non-blocking operations, such as when promises never resolve (as shown in the example test file above). First, I allowed a timeout to be specified in Deno.test({
name: "never resolves",
fn: async () => { await willResolve(false) },
timeout: 5000
});
Deno.test("never resolves", async () => {
await willResolve(false)
}, 5000);
Deno.test(function neverResolves() => {
await willResolve(false)
}, 5000); By default, the tests will use a timeout of Then, when running the tests, we kick off a timer and immediately start executing the test. We then for (const test of filtered) {
// ...
const timeout = new Promise((resolve) => setTimeout(() => {
resolve({
"failed": formatError(
new TestTimeoutError(`Test timed out after ${test.timeout}ms`)
)
});
}, test.timeout));
const testExecution = runTest(test, description);
const result = await Promise.race([testExecution, timeout]);
// ...
reportTestResult(description, result, elapsed);
}
Deno.test("blocking test", () => {
while (true) {}
}); The ideal approachAs mentioned by @caspervonb, it would be much better to monitor a test's execution from within the Rust runtime so that we could throw errors even when blocking operations cause a test to exceed a timeout. Having read through the code, however, I believe there would be a significant amount of work to enable detecting timeout for individual synchronous tests. For us to be able to timeout individual synchronous tests we'd need to change the unit of execution to that of a single test, not to Lines 513 to 522 in 9872fbf
We cannot timeout individual tests because the JS runtime which is running all of the specifier's tests cannot be preempted to move forward once synchronous operations are in place. If there are synchronous operations happening, the stack never empties, and thus there's no opportunity to pull anything from the event loop. Timing out an entire specifier, however, is way easier. For that, we could simply start a timer as the test starts and, if the timer gets triggered before the test's end, we terminate the specifier's execution. When that happens, we then mark all remaining tests as having failed. Lines 852 to 854 in 9872fbf
Proposed way forwardUnless I'm missing something here, there seems to be no way of timing out a single synchronous test unless we coordinate the whole specifier execution within Rust, and delegate to JS only the execution of individual tests. Considering how much work the aforementioned refactor would involve, I suggest we break down this issue into two:
In case others have better ideas on how to detect and timeout individual synchronous tests, please let me know. I'm quite new to Deno and Rust, so I might not be aware of other ways of doing it, and I'm also keen to learn from others. |
We can time out individual tests easily by enumerating the deferred tests on the Rust side, I've added APIs in core to make this cleaner to do but the addition of the immediate mode steps API kind of borked all my plans to make the test runner any better and I can't really progress forward with it at this point. I've moved further development into my own runtime. |
Hi @caspervonb, thank you for taking the time to clarify 💖 I totally see how we could enumerate the deferred tests on the Rust side, but those would only be non-blocking tests, correct? I totally agree we should time out async tests in Rust rather than in JS as I've done in my example, but I still can't see how we would time-out individual blocking tests within a specifier. Consider, for example, a file with the following contents: Deno.test("blocking test", () => {
while (true) {}
});
Deno.test("non-blocking test", () => {}); In this case, because Rust invokes That's why I thought that we could have different isolates/threads per test: so that we could timeout individual tests. |
The test handler runs on its own thread by design so timeouts, amongst other things can still be handled.
Even with a single isolate one could call terminate execution in v8, and then continue with the next test. Bad state can of course have a cascading effect if you write your tests in a leaky manner but not really a concern of the runner. Another path is to simply do nothing and print warnings ala "hey this test is hanging after " with no side effects. Simple is often better, this is what libtest (cargo test) does. |
As discussed on Discord, I started implementing a simple timeout handled in the JS runtime as described in my previous comment. For future reference, here's what we discussed:
After implementing the async timeout, I'll start working on a separate PR for reporting a timeout warning for synchronous tests using Rust, as @caspervonb suggested.
|
If we do implement this, I think we should have a solution right away that works for both sync and async tests. A synchronous solution would also work for an async one. I like Casper's suggestion to show a warning like Rust. Perhaps it should show if it has been X seconds since the test started OR the last test step was reported. |
I think this would be a good idea. Rust should start a JavaScript interpreter and tell it to run each test one at a time. This could also be used to run tests in parallel, using multiple JavaScript interpreters. If one test is slow or hangs, only one interpreter is affected and the other tests can keep going. It also might be a way to improve isolation between tests. When a test fails, throw away that JavaScript interpreter and create another one, so it can't affect any other tests. (For performance, it might be good to have a spare interpreter ready.) There could be a "maximum paranoia" mode where Rust only runs one test in an interpreter before throwing it away. However, that would be slow when there's a lot of initialization before an interpreter can start running the first test. |
I'm sharing my solution: import { afterEach, beforeEach } from 'std/testing/bdd.ts'
function withTimeout(timeoutMs = 5000) {
let timeoutId: number
beforeEach(() => {
timeoutId = setTimeout(() => {
throw new Error(`Timed out after ${timeoutMs} ms.`)
}, timeoutMs)
})
afterEach(() => {
clearTimeout(timeoutId)
})
} |
Trying to work around this by passing Deno.test('NRelay1.query', async () => {
const relay = new NRelay1('wss://relay.mostr.pub');
const events = await relay.query([{ kinds: [1], limit: 3 }], { signal: AbortSignal.timeout(1000) });
assertEquals(events.length, 3);
await relay.close();
});
Unfortunately there is no easy way to get rid of the timeout attached to the AbortSignal when you create it with |
This is completely untested and I'm not super familiar with web apis, but you might want to create a wrapper around class TestTimeout {
#timerId: ReturnType<typeof setTimeout>;
#controller: AbortController;
constructor(time: number) {
this.#controller = new AbortController();
this.#timerId = setTimeout(() => {
this.#controller.abort();
}, time);
}
[Symbol.dispose]() {
clearTimeout(this.#timerId);
}
signal() {
return this.#controller.signal;
}
} Then: Deno.test('NRelay1.query', async () => {
using timeout = new TestTimeout(1000);
const relay = new NRelay1('wss://relay.mostr.pub');
const events = await relay.query([{ kinds: [1], limit: 3 }], { signal: timeout.signal });
assertEquals(events.length, 3);
await relay.close();
}); |
FYI, according to @mmastrac there are no more technical blockers to support this feature for both async and sync code (including sync hot-loops). I spoke with @nathanwhit about this and it's been put in the backlog, but due to incoming Deno 2 release we might not have a bandwidth to address it for some time still. |
Deno.TestDefinition
should allows to specify a timeout for a test case:The text was updated successfully, but these errors were encountered: