Skip to content
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

Test runner: spurious "Promise resolution is still pending but the event loop has already resolved" when a callback tests fails #51381

Closed
domenic opened this issue Jan 5, 2024 · 9 comments · Fixed by #51996
Labels
confirmed-bug Issues with confirmed bugs. test_runner Issues and PRs related to the test runner subsystem.

Comments

@domenic
Copy link
Contributor

domenic commented Jan 5, 2024

Version

v21.4.0

Platform

Microsoft Windows NT 10.0.22621.0 x64

Subsystem

test

What steps will reproduce the bug?

Run the following code with node --test test.js:

"use strict";
const { describe, test } = require("node:test");
const assert = require("node:assert");

describe("describe wrapper", () => {
  test("callback test", (t, done) => {
    setTimeout(() => {
      assert.ok(false, "oh no an assert failed");
      done();
    });
  });

  test("promise test", async () => {
    assert.ok(true, "this assert will pass");
  });
});

describe("another describe block", () => {
  test("sync test", () => {
    assert.ok(true, "this assert will pass 2");
  });
});

How often does it reproduce? Is there a required condition?

Always

What is the expected behavior? Why is that the expected behavior?

It should tell me that "callback test" alone is failing

What do you see instead?

It shows four failure messages: one for callback test (correct), one for describe wrapper, one for sync test, and one for another describe block. The latter three have a (green??) 'Promise resolution is still pending but the event loop has already resolved' failure reason.

$ node --test test.js
▶ describe wrapper
  ✖ callback test (2.4042ms)
    AssertionError [ERR_ASSERTION]: oh no an assert failed
        at Timeout._onTimeout (C:\Users\d\OneDrive - domenic.me\Code\GitHub\jsdom\jsdom\test.js:8:14)
        at listOnTimeout (node:internal/timers:573:17)
        at process.processTimers (node:internal/timers:514:7) {
      generatedMessage: false,
      code: 'ERR_ASSERTION',
      actual: false,
      expected: true,
      operator: '=='
    }

  ✔ promise test (0.2519ms)
▶ describe wrapper (5.4167ms)

  'Promise resolution is still pending but the event loop has already resolved'

▶ another describe block
  ✖ sync test
    'Promise resolution is still pending but the event loop has already resolved'

▶ another describe block

  'Promise resolution is still pending but the event loop has already resolved'

ℹ tests 3
ℹ suites 2
ℹ pass 1
ℹ fail 1
ℹ cancelled 1
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 47.7459

✖ failing tests:

test at test.js:6:3
✖ callback test (2.4042ms)
  AssertionError [ERR_ASSERTION]: oh no an assert failed
      at Timeout._onTimeout (C:\Users\d\OneDrive - domenic.me\Code\GitHub\jsdom\jsdom\test.js:8:14)
      at listOnTimeout (node:internal/timers:573:17)
      at process.processTimers (node:internal/timers:514:7) {
    generatedMessage: false,
    code: 'ERR_ASSERTION',
    actual: false,
    expected: true,
    operator: '=='
  }

test at test.js:5:1
✖ describe wrapper (5.4167ms)
  'Promise resolution is still pending but the event loop has already resolved'

test at test.js:19:3
✖ sync test
  'Promise resolution is still pending but the event loop has already resolved'

test at test.js:18:1
✖ another describe block
  'Promise resolution is still pending but the event loop has already resolved'

Additional information

There isn't a lot of documentation about callback-style tests...

@marco-ippolito
Copy link
Member

marco-ippolito commented Jan 5, 2024

You can just remove done and it will work as expected.

  test("callback test", () => {
    setTimeout(() => {
      assert.ok(false, "oh no an assert failed");
    });
  });

AFAIK done is useful when you have function thats take a callback and you want to check that it is called or when for some reason can't use assertions:

const { describe, it } = require("node:test");
const assert = require("node:assert");
const fs = require('node:fs');

describe("describe wrapper", async () => {
    it("callback test", (t, done) => {
        fs.readFile('package.json', (err, data) => {
            if (err) done(err); // will fail with [Error: ENOENT: no such file or directory
            else assert.ok(data);
        });
    }, 0);
});

PRs to improve the documentation are always welcomed

@marco-ippolito marco-ippolito added the test_runner Issues and PRs related to the test runner subsystem. label Jan 5, 2024
@domenic
Copy link
Contributor Author

domenic commented Jan 5, 2024

That doesn't quite work as expected. In that case, the error gets thrown disconnected from any test, so it looks like all tests are passing, but then there's a mysterious 'test failed' (again, in green?) and a discussion about generating async activity.

Modified script
"use strict";
const { describe, test } = require("node:test");
const assert = require("node:assert");

describe("describe wrapper", () => {
  test("callback test", () => {
    setTimeout(() => {
      assert.ok(false, "oh no an assert failed");
    });
  });

  test("promise test", async () => {
    assert.ok(true, "this assert will pass");
  });
});

describe("another describe block", () => {
  test("sync test", () => {
    assert.ok(true, "this assert will pass 2");
  });
});

Output:

$ node --test test.js
▶ describe wrapper
  ✔ callback test (0.3599ms)
  ✔ promise test (0.1025ms)
▶ describe wrapper (1.6332ms)

▶ another describe block
  ✔ sync test (0.0629ms)
▶ another describe block (0.193ms)

ℹ Warning: Test "callback test" generated asynchronous activity after the test ended. This activity created the error "AssertionError [ERR_ASSERTION]: oh no an
assert failed" and would have caused the test to fail, but instead triggered an uncaughtException event.
✖ test.js (41.915ms)
  'test failed'

ℹ tests 4
ℹ suites 2
ℹ pass 3
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 47.6652

✖ failing tests:

test at test.js:1:1
✖ test.js (41.915ms)
  'test failed'

My expectation is that callback tests wait on the callback to be called, or an error to be thrown, in order to tie any thrown errors to the in-progress test. That's how they work in other test frameworks.

@benjamingr
Copy link
Member

benjamingr commented Jan 5, 2024

@nodejs/test_runner @MoLow I think in any case the callback test failing shouldn't interfere with any of the other tests.

Re: the second example - The "fail the test file for an uncaught error" behavior actually seems reasonable to me.

@benjamingr benjamingr added the confirmed-bug Issues with confirmed bugs. label Jan 5, 2024
@MoLow
Copy link
Member

MoLow commented Jan 6, 2024

@nodejs/test_runner @MoLow I think in any case the callback test failing shouldn't interfere with any of the other tests.

Re: the second example - The "fail the test file for an uncaught error" behavior actually seems reasonable to me.

I agree with both these statments

@marco-ippolito
Copy link
Member

If the test goes in 'uncaughtException' it's not possible to recover, so it will interfere with other test.

The correct use of 'uncaughtException' is to perform synchronous cleanup of allocated resources
(e.g. file descriptors, handles, etc) before shutting down the process.
It is not safe to resume normal operation after 'uncaughtException'.

I agree with the second statement

@domenic
Copy link
Contributor Author

domenic commented Feb 4, 2024

In other test frameworks, the framework is responsible for catching the exception, so it never reaches uncaughtException.

@cjihrig
Copy link
Contributor

cjihrig commented Feb 9, 2024

This seems to fix the issue. The problem is that this never finishes.

diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js
index 469ca903c7..b62d4dded6 100644
--- a/lib/internal/test_runner/harness.js
+++ b/lib/internal/test_runner/harness.js
@@ -74,7 +74,7 @@ function createProcessEventHandler(eventName, rootTest) {
     }
 
     test.fail(new ERR_TEST_FAILURE(err, eventName));
-    test.postRun();
+    test.abortController.abort();
   };
 }

It might also be worth considering ref()'ing this timer or implementing another waiting technique, because currently, the following code does not honor its timeout.

const { test } = require('node:test');

test({ timeout: 3000 }, (t, done) => {
  // done() is not called but there are no ref()'ed handles so we exit.
});

It might also be worth considering setting a reasonable default test timeout instead of Infinity for the same reason.

Speaking of test timeouts, the new --test-timeout CLI flag behaves surprisingly. Consider the following code run with --test-timeout=2000. It correctly times out when used with --test, but passes without --test. I believe that's because it is not incorporated into parseCommandLine() and the logic that uses it.

const { test } = require('node:test');

test((t, done) => {
  setTimeout(done, 3000);
});

cjihrig added a commit to cjihrig/node that referenced this issue Mar 7, 2024
This commit updates the test runner's uncaughtException handler
to abort tests instead of assuming they finished running.

Fixes: nodejs#51381
nodejs-github-bot pushed a commit that referenced this issue Mar 9, 2024
This commit updates the test runner's uncaughtException handler
to abort tests instead of assuming they finished running.

Fixes: #51381
PR-URL: #51996
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
@cjihrig cjihrig reopened this Mar 18, 2024
@cjihrig cjihrig closed this as completed Mar 19, 2024
rdw-msft pushed a commit to rdw-msft/node that referenced this issue Mar 26, 2024
This commit updates the test runner's uncaughtException handler
to abort tests instead of assuming they finished running.

Fixes: nodejs#51381
PR-URL: nodejs#51996
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
marco-ippolito pushed a commit that referenced this issue May 2, 2024
This commit updates the test runner's uncaughtException handler
to abort tests instead of assuming they finished running.

Fixes: #51381
PR-URL: #51996
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
jcbhmr pushed a commit to jcbhmr/node that referenced this issue May 15, 2024
This commit updates the test runner's uncaughtException handler
to abort tests instead of assuming they finished running.

Fixes: nodejs#51381
PR-URL: nodejs#51996
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
@TechQuery
Copy link

TechQuery commented Aug 24, 2024

I find a similar bug that Node.js test runner works well in Codespaces and throw errors just like this issue in GitHub actions, they are all running in latest Linux + Node.js 20:

@RedYetiDev
Copy link
Member

Hey, this issue has been resolved for several months. If you're experiencing an issue, please open a new issue or visit nodejs/help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
confirmed-bug Issues with confirmed bugs. test_runner Issues and PRs related to the test runner subsystem.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants