Enable browser tests to be run under node

In some cases, specifically thread tests, it can be useful to expriment
with running browser tests under node.

This change add a new special value to EMTEST_BROSWER that will cause
`btest` to attempt to run the test under node.

I've also adding since browser test to the wasm2-test suite in circle CI
so this features gets tested.
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 1a0a621..cbbbb6c 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -364,10 +364,12 @@
           test_targets: "wasm0"
   test-wasm2:
     executor: bionic
+    environment:
+      EMTEST_BROWSER: "node"
     steps:
       - run-tests:
-          # also add a few asan tests
-          test_targets: "wasm2 asan.test_embind* asan.test_abort_on_exceptions asan.test_ubsan_full_left_shift_fsanitize_integer asan.test_pthread* asan.test_dyncall_specific_minimal_runtime"
+          # also add a few asan tests and a single test of EMTEST_BROWSER=node
+          test_targets: "wasm2 asan.test_embind* asan.test_abort_on_exceptions asan.test_ubsan_full_left_shift_fsanitize_integer asan.test_pthread* asan.test_dyncall_specific_minimal_runtime browser.test_pthread_join"
   test-wasm3:
     executor: bionic
     steps:
diff --git a/tests/browser_reporting.js b/tests/browser_reporting.js
index 125fa9e..6c3c1c9 100644
--- a/tests/browser_reporting.js
+++ b/tests/browser_reporting.js
@@ -9,13 +9,20 @@
     reportErrorToServer("excessive reported results, sending " + result + ", test will fail");
   }
   reportResultToServer.reported = true;
-  var xhr = new XMLHttpRequest();
-  if (hasModule && Module['pageThrewException']) {
-    result = 'pageThrewException';
+  if (ENVIRONMENT_IS_NODE) {
+    out('RESULT: ' + result);
+  } else {
+    var xhr = new XMLHttpRequest();
+    if (hasModule && Module['pageThrewException']) {
+      result = 'pageThrewException';
+    }
+    xhr.open('GET', 'http://localhost:' + port + '/report_result?' + result, !sync);
+    xhr.send();
+    /* for easy debugging, don't close window on failure */
+    if (typeof window === 'object' && window && hasModule && !Module['pageThrewException']) {
+      setTimeout(function() { window.close() }, 1000);
+    }
   }
-  xhr.open('GET', 'http://localhost:' + port + '/report_result?' + result, !sync);
-  xhr.send();
-  if (typeof window === 'object' && window && hasModule && !Module['pageThrewException'] /* for easy debugging, don't close window on failure */) setTimeout(function() { window.close() }, 1000);
 }
 
 /** @param {boolean=} sync
@@ -27,8 +34,12 @@
 
 function reportErrorToServer(message) {
   var xhr = new XMLHttpRequest();
-  xhr.open('GET', encodeURI('http://localhost:8888?stderr=' + message));
-  xhr.send();
+  if (ENVIRONMENT_IS_NODE) {
+    err(message);
+  } else {
+    xhr.open('GET', encodeURI('http://localhost:8888?stderr=' + message));
+    xhr.send();
+  }
 }
 
 if (typeof window === 'object' && window) {
diff --git a/tests/common.py b/tests/common.py
index b0a45cc..1688236 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -40,8 +40,14 @@
 
 # User can specify an environment variable EMTEST_BROWSER to force the browser
 # test suite to run using another browser command line than the default system
-# browser.  Setting '0' as the browser disables running a browser (but we still
-# see tests compile)
+# browser.
+# There are two special value that can be used here if running in an actual
+# browser is not desired:
+#  EMTEST_BROWSER=0 : This will disable the actual running of the test and simply
+#                     verify that it compiles and links.
+#  EMTEST_BROWSER=node : This will attempt to run the browser test under node.
+#                        For most browser tests this does not work, but it can
+#                        be useful for running pthread tests under node.
 EMTEST_BROWSER = None
 EMTEST_DETECT_TEMPFILE_LEAKS = None
 EMTEST_SAVE_DIR = None
@@ -1299,7 +1305,7 @@
     super().setUpClass()
     cls.also_asmjs = int(os.getenv('EMTEST_BROWSER_ALSO_ASMJS', '0')) == 1
     cls.port = int(os.getenv('EMTEST_BROWSER_PORT', '8888'))
-    if not has_browser():
+    if not has_browser() or EMTEST_BROWSER == 'node':
       return
     cls.browser_timeout = 60
     cls.harness_in_queue = multiprocessing.Queue()
@@ -1312,7 +1318,7 @@
   @classmethod
   def tearDownClass(cls):
     super().tearDownClass()
-    if not has_browser():
+    if not has_browser() or EMTEST_BROWSER == 'node':
       return
     cls.harness_server.terminate()
     print('[Browser harness server terminated]')
@@ -1512,6 +1518,8 @@
         args += ['-I' + TEST_ROOT,
                  '-include', test_file('report_result.h'),
                  test_file('report_result.cpp')]
+    if EMTEST_BROWSER == 'node':
+      args.append('-DEMTEST_NODE')
     self.run_process([EMCC] + self.get_emcc_args() + args)
 
   def btest_exit(self, filename, assert_returncode=0, *args, **kwargs):
@@ -1554,7 +1562,13 @@
       post_build()
     if not isinstance(expected, list):
       expected = [expected]
-    self.run_browser(outfile + url_suffix, message, ['/report_result?' + e for e in expected], timeout=timeout, extra_tries=extra_tries)
+    if EMTEST_BROWSER == 'node':
+      self.js_engines = [config.NODE_JS]
+      self.node_args += ['--experimental-wasm-threads', '--experimental-wasm-bulk-memory']
+      output = self.run_js('test.js')
+      self.assertContained('RESULT: ' + expected[0], output)
+    else:
+      self.run_browser(outfile + url_suffix, message, ['/report_result?' + e for e in expected], timeout=timeout, extra_tries=extra_tries)
 
     # Tests can opt into being run under asmjs as well
     if 'WASM=0' not in original_args and (also_asmjs or self.also_asmjs):
diff --git a/tests/pthread/test_pthread_create.cpp b/tests/pthread/test_pthread_create.cpp
index 901f6c8..ff89809 100644
--- a/tests/pthread/test_pthread_create.cpp
+++ b/tests/pthread/test_pthread_create.cpp
@@ -81,8 +81,8 @@
 		CreateThread(i);
 
 	// Join all threads and create more.
-        while (numThreadsToCreate > 0)
-        {
+	while (numThreadsToCreate > 0)
+	{
 		for(int i = 0; i < NUM_THREADS; ++i)
 		{
 			if (thread[i])
@@ -101,5 +101,6 @@
 			}
 		}
 	}
+	printf("All threads joined.\n");
 	return 0;
 }
diff --git a/tests/report_result.cpp b/tests/report_result.cpp
index 630c5c5..fe1ff61 100644
--- a/tests/report_result.cpp
+++ b/tests/report_result.cpp
@@ -6,35 +6,55 @@
  */
 
 #include <stdio.h>
+#include <stdlib.h>
 
-#ifdef __EMSCRIPTEN__
+#include "report_result.h"
 
+#if defined __EMSCRIPTEN__ && !defined EMTEST_NODE
 #include <emscripten.h>
-
-#ifndef EMTEST_PORT_NUMBER
-#error "EMTEST_PORT_NUMBER not defined"
 #endif
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
-void EMSCRIPTEN_KEEPALIVE _ReportResult(int result, int sync)
-{
+#if defined __EMSCRIPTEN__ && !defined EMTEST_NODE
+#ifndef EMTEST_PORT_NUMBER
+#error "EMTEST_PORT_NUMBER not defined"
+#endif
+
+void EMSCRIPTEN_KEEPALIVE _ReportResult(int result, int sync) {
   EM_ASM({
     reportResultToServer($0, $1, $2);
   }, result, sync, EMTEST_PORT_NUMBER);
 }
 
-void EMSCRIPTEN_KEEPALIVE _MaybeReportResult(int result, int sync)
-{
+void EMSCRIPTEN_KEEPALIVE _MaybeReportResult(int result, int sync) {
   EM_ASM({
     maybeReportResultToServer($0, $1, $2);
   }, result, sync, EMTEST_PORT_NUMBER);
 }
 
+#else
+
+static bool reported = false;
+
+void _ReportResult(int result, int sync) {
+  if (reported) {
+    printf("ERROR: result already reported\n");
+    exit(1);
+  }
+  reported = true;
+  printf("RESULT: %d\n", result);
+}
+
+void _MaybeReportResult(int result, int sync) {
+  if (!reported) _ReportResult(result, sync);
+}
+
+#endif // __EMSCRIPTEN__ && !defined EMTEST_NODE
+
 #ifdef __cplusplus
 }
 #endif
 
-#endif // __EMSCRIPTEN__
diff --git a/tests/report_result.h b/tests/report_result.h
index 2012bdf..cd0ceca 100644
--- a/tests/report_result.h
+++ b/tests/report_result.h
@@ -10,8 +10,6 @@
 #ifndef REPORT_RESULT_H_
 #define REPORT_RESULT_H_
 
-#ifdef __EMSCRIPTEN__
-
 #ifdef __cplusplus
 extern "C" {
 #endif
@@ -23,7 +21,8 @@
 }
 #endif
 
-#if __EMSCRIPTEN_PTHREADS__
+#if defined __EMSCRIPTEN__ && defined __EMSCRIPTEN_PTHREADS__
+  #include <emscripten.h>
   #include <emscripten/threading.h>
   #define REPORT_RESULT(result) emscripten_async_run_in_main_runtime_thread(EM_FUNC_SIG_VII, _ReportResult, (result), 0)
   #define REPORT_RESULT_SYNC(result) emscripten_sync_run_in_main_runtime_thread(EM_FUNC_SIG_VII, _ReportResult, (result), 1)
@@ -36,19 +35,4 @@
   #define MAYBE_REPORT_RESULT_SYNC(result) _MaybeReportResult((result), 1)
 #endif
 
-#else
-
-#include <stdio.h>
-#include <stdlib.h>
-
-#define REPORT_RESULT(result)       \
-  do {                              \
-    printf("result: %d\n", result); \
-    exit(result);                   \
-  }
-
-#define REPORT_RESULT_SYNC REPORT_RESULT
-
-#endif // __EMSCRIPTEN__
-
 #endif // REPORT_RESULT_H_
diff --git a/tests/sdl2_net_client.c b/tests/sdl2_net_client.c
index 90293ca..4610814 100644
--- a/tests/sdl2_net_client.c
+++ b/tests/sdl2_net_client.c
@@ -24,7 +24,7 @@
 #include "SDL_net.h"
 
 #ifdef __EMSCRIPTEN__
-#include <emscripten.h>
+#include <emscripten/emscripten.h>
 #endif
 
 typedef enum {
diff --git a/tests/test_browser.py b/tests/test_browser.py
index 9a4c4d8..c6465d6 100644
--- a/tests/test_browser.py
+++ b/tests/test_browser.py
@@ -160,9 +160,10 @@
   def setUpClass(cls):
     super().setUpClass()
     cls.browser_timeout = 60
-    print()
-    print('Running the browser tests. Make sure the browser allows popups from localhost.')
-    print()
+    if EMTEST_BROWSER != 'node':
+      print()
+      print('Running the browser tests. Make sure the browser allows popups from localhost.')
+      print()
 
   def setUp(self):
     super().setUp()