diff --git a/test/common.py b/test/common.py index 3ae5119d50620..5e485286aea6f 100644 --- a/test/common.py +++ b/test/common.py @@ -17,7 +17,7 @@ import itertools import json import logging -import multiprocessing +import threading import os import re import shlex @@ -2210,10 +2210,10 @@ def get_zlib_library(self, cmake): return rtn -# Run a server and a web page. When a test runs, we tell the server about it, +# Create a server and a web page. When a test runs, we tell the server about it, # which tells the web page, which then opens a window with the test. Doing # it this way then allows the page to close() itself when done. -def harness_server_func(in_queue, out_queue, port): +def make_test_server(in_queue, out_queue, port): class TestServerHandler(SimpleHTTPRequestHandler): # Request header handler for default do_GET() path in # SimpleHTTPRequestHandler.do_GET(self) below. @@ -2313,10 +2313,6 @@ def do_GET(self): else: self.send_error(400, f'Not implemented for {code}') elif 'report_' in self.path: - # the test is reporting its result. first change dir away from the - # test dir, as it will be deleted now that the test is finishing, and - # if we got a ping at that time, we'd return an error - os.chdir(path_from_root()) # for debugging, tests may encode the result and their own url (window.location) as result|url if '|' in self.path: path, url = self.path.split('|', 1) @@ -2358,8 +2354,6 @@ def do_GET(self): assert out_queue.empty(), 'the single response from the last test was read' # tell the browser to load the test self.wfile.write(b'COMMAND:' + url.encode('utf-8')) - # move us to the right place to serve the files for the new test - os.chdir(dir) else: # the browser must keep polling self.wfile.write(b'(wait)') @@ -2395,8 +2389,23 @@ def log_request(code=0, size=0): # do not have the correct MIME type SimpleHTTPRequestHandler.extensions_map['.mjs'] = 'text/javascript' - httpd = ThreadingHTTPServer(('localhost', port), TestServerHandler) - httpd.serve_forever() # test runner will kill us + return ThreadingHTTPServer(('localhost', port), TestServerHandler) + + +class HttpServerThread(threading.Thread): + """A generic thread class to create and run an http server.""" + def __init__(self, server): + super().__init__() + self.server = server + + def stop(self): + """Shuts down the server if it is running.""" + self.server.shutdown() + + def run(self): + """Creates the server instance and serves forever until stop() is called.""" + # Start the server's main loop (this blocks until shutdown() is called) + self.server.serve_forever() class Reporting(Enum): @@ -2488,11 +2497,13 @@ def setUpClass(cls): super().setUpClass() if not has_browser() or EMTEST_BROWSER == 'node': return - cls.harness_in_queue = multiprocessing.Queue() - cls.harness_out_queue = multiprocessing.Queue() - cls.harness_server = multiprocessing.Process(target=harness_server_func, args=(cls.harness_in_queue, cls.harness_out_queue, cls.PORT)) + + cls.harness_in_queue = queue.Queue() + cls.harness_out_queue = queue.Queue() + cls.harness_server = HttpServerThread(make_test_server(cls.harness_in_queue, cls.harness_out_queue, cls.PORT)) cls.harness_server.start() - print('[Browser harness server on process %d]' % cls.harness_server.pid) + + print(f'[Browser harness server on thread {cls.harness_server.name}]') cls.browser_open(cls.HARNESS_URL) @classmethod @@ -2500,9 +2511,10 @@ def tearDownClass(cls): super().tearDownClass() if not has_browser() or EMTEST_BROWSER == 'node': return - cls.harness_server.terminate() - print('[Browser harness server terminated]') + cls.harness_server.stop() + cls.harness_server.join() cls.browser_terminate() + if WINDOWS: # On Windows, shutil.rmtree() in tearDown() raises this exception if we do not wait a bit: # WindowsError: [Error 32] The process cannot access the file because it is being used by another process. diff --git a/test/test_browser.py b/test/test_browser.py index d98849e9bf933..c61d7328bb4dd 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -4,7 +4,6 @@ # found in the LICENSE file. import argparse -import multiprocessing import os import random import shlex @@ -25,14 +24,17 @@ from common import read_file, EMRUN, no_wasm64, no_2gb, no_4gb, copytree from common import requires_wasm2js, parameterize, find_browser_test_file, with_all_sjlj from common import also_with_minimal_runtime, also_with_wasm2js, also_with_asan, also_with_wasmfs +from common import HttpServerThread from tools import shared from tools import ports from tools.shared import EMCC, WINDOWS, FILE_PACKAGER, PIPE, DEBUG from tools.utils import delete_dir -def test_chunked_synchronous_xhr_server(support_byte_ranges, data, port): +def make_test_chunked_synchronous_xhr_server(support_byte_ranges, data, port): class ChunkedServerHandler(BaseHTTPRequestHandler): + num_get_connections = 0 + def sendheaders(s, extra=None, length=None): length = length or len(data) s.send_response(200) @@ -56,6 +58,11 @@ def do_OPTIONS(s): s.sendheaders([("Access-Control-Allow-Headers", "Range")], 0) def do_GET(s): + # CORS preflight makes OPTIONS requests which we need to account for. + expectedConns = 22 + s.num_get_connections += 1 + assert(s.num_get_connections < expectedConns) + if s.path == '/': s.sendheaders() elif not support_byte_ranges: @@ -70,11 +77,7 @@ def do_GET(s): s.sendheaders([], length) s.wfile.write(data[start:end + 1]) - # CORS preflight makes OPTIONS requests which we need to account for. - expectedConns = 22 - httpd = HTTPServer(('localhost', 11111), ChunkedServerHandler) - for _ in range(expectedConns + 1): - httpd.handle_request() + return HTTPServer(('localhost', 11111), ChunkedServerHandler) def also_with_proxying(f): @@ -1703,7 +1706,7 @@ def test_chunked_synchronous_xhr(self): data = os.urandom(10 * chunkSize + 1) # 10 full chunks and one 1 byte chunk checksum = zlib.adler32(data) & 0xffffffff # Python 2 compatibility: force bigint - server = multiprocessing.Process(target=test_chunked_synchronous_xhr_server, args=(True, data, self.PORT)) + server = HttpServerThread(make_test_chunked_synchronous_xhr_server(True, data, self.PORT)) server.start() # block until the server is actually ready @@ -1720,7 +1723,8 @@ def test_chunked_synchronous_xhr(self): try: self.run_browser(main, '/report_result?' + str(checksum)) finally: - server.terminate() + server.stop() + server.join() # Avoid race condition on cleanup, wait a bit so that processes have released file locks so that test tearDown won't # attempt to rmdir() files in use. if WINDOWS: