Repository: nodejs/node
Pull request: test_runner: support handling errors from outside of tests

##### test_runner: give the root test a harness reference

This commit replaces the 'coverage' reference inside of the Test
class with a more generic harness reference which includes
coverage. This will let the root test more easily track process
level state such as code coverage, uncaughtException handlers,
and the state of bootstrapping.

##### test_runner: track bootstrapping process

This commit updates the test harness and root test to track
when bootstrapping has completed.

##### test_runner: throw if harness is not bootstrapped

This commit updates the test harness to re-throw uncaught errors
if bootstrapping has not completed. This updates the existing
logic which tried to detect a specific error code.

##### test_runner: handle errors not bound to tests

This commit addresses a previously untested branch of the code.
It is possible when using the test runner that an error occurs
outside of a test. In this case, the test runner would simply
rethrow the error. This commit updates the logic to handle the
error in the same fashion as other uncaughtExceptions.


Changed files:
filename: lib/internal/test_runner/harness.js
patch: @@ -43,17 +43,21 @@ function createProcessEventHandler(eventName, rootTest) {
     // Check if this error is coming from a test. If it is, fail the test.
     const test = testResources.get(executionAsyncId());
 
-    if (!test) {
-      throw err;
-    }
-
-    if (test.finished) {
-      // If the test is already finished, report this as a top level
-      // diagnostic since this is a malformed test.
-      const msg = `Warning: Test "${test.name}" generated asynchronous ` +
-        'activity after the test ended. This activity created the error ' +
-        `"${err}" and would have caused the test to fail, but instead ` +
-        `triggered an ${eventName} event.`;
+    if (!test || test.finished) {
+      // If the test is already finished or the resource that created the error
+      // is not mapped to a Test, report this as a top level diagnostic.
+      let msg;
+
+      if (test) {
+        msg = `Warning: Test "${test.name}" generated asynchronous ` +
+          'activity after the test ended. This activity created the error ' +
+          `"${err}" and would have caused the test to fail, but instead ` +
+          `triggered an ${eventName} event.`;
+      } else {
+        msg = 'Warning: A resource generated asynchronous activity after ' +
+          `the test ended. This activity created the error "${err}" which ` +
+          `triggered an ${eventName} event, caught by the test runner.`;
+      }
 
       rootTest.diagnostic(msg);
       process.exitCode = kGenericUserError;

filename: test/message/test_runner_output.js
patch: @@ -383,3 +383,9 @@ test('unfinished test with unhandledRejection', async () => {
     setTimeout(() => Promise.reject(new Error('bar')));
   });
 });
+
+// Verify that uncaught exceptions outside of any tests are handled after the
+// test harness has finished bootstrapping itself.
+setImmediate(() => {
+  throw new Error('uncaught from outside of a test');
+});

filename: test/message/test_runner_output.out
patch: @@ -637,6 +637,7 @@ not ok 65 - invalid subtest fail
 1..65
 # Warning: Test "unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
 # Warning: Test "async unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
+# Warning: A resource generated asynchronous activity after the test ended. This activity created the error "Error: uncaught from outside of a test" which triggered an uncaughtException event, caught by the test runner.
 # Warning: Test "immediate throw - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event.
 # Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
 # Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event.

filename: test/message/test_runner_output_cli.out
patch: @@ -636,6 +636,7 @@ not ok 65 - invalid subtest fail
   ...
 # Warning: Test "unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
 # Warning: Test "async unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
+# Warning: A resource generated asynchronous activity after the test ended. This activity created the error "Error: uncaught from outside of a test" which triggered an uncaughtException event, caught by the test runner.
 # Warning: Test "immediate throw - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event.
 # Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
 # Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event.

filename: test/message/test_runner_output_spec_reporter.out
patch: @@ -270,6 +270,7 @@
 
  Warning: Test "unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
  Warning: Test "async unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
+ Warning: A resource generated asynchronous activity after the test ended. This activity created the error "Error: uncaught from outside of a test" which triggered an uncaughtException event, caught by the test runner.
  Warning: Test "immediate throw - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event.
  Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
  Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event.