blob: 0892226b7464373c8950f08903598104d76feecf [file]
# Copyright 2021 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import json
from six.moves import BaseHTTPServer
from six.moves import urllib
import sys
from tempfile import mkstemp
import threading
import os
import logging
# This script is run via //third_party/blink/tools/run_webgpu_cts.py which
# adds blinkpy to the Python path.
from blinkpy.common import path_finder
from blinkpy.common.host import Host
from blinkpy.web_tests import run_web_tests
path_finder.add_typ_dir_to_sys_path()
from typ.expectations_parser import TaggedTestListParser, Expectation
from typ.json_results import ResultType
# Basic HTTP request handler to serve the WebGPU webgpuCtsExpectations.js file
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def __init__(self, *args, **kwargs):
BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
def do_GET(self):
if self.path == "/webgpuCtsExpectations.js":
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Content-Type', 'application/javascript')
self.end_headers()
self.wfile.write(self.server.expectations_js.encode())
elif self.path == '/_start' or self.path == '/_stop':
self.send_response(200)
self.end_headers()
else:
self.send_response(404)
self.end_headers()
self.wfile.write('Not found')
# Basic HTTP server which handles request using RequestHandler on a background thread.
class ExpectationsServer(BaseHTTPServer.HTTPServer):
def __init__(self, expectations_js, server_address):
BaseHTTPServer.HTTPServer.__init__(self, server_address,
RequestHandler)
self._should_run = False
self._thread = None
self.expectations_js = expectations_js
def start(self):
assert not self._thread
self._should_run = True
def _loop():
while self._should_run:
self.handle_request()
self._thread = threading.Thread(name='webgpu_expectations_server',
target=_loop)
# Mark the thread as a daemon to be sure it exits. We still have an explicit
# |stop| method because daemon threads are stopped abruptly at shutdown without
# cleaning up resources.
self._thread.daemon = True
self._thread.start()
# Ensure the server is running.
# We want to wait synchronously for this so that server startup time doesn't
# cut into test run time.
while True:
try:
urllib.request.urlopen(
'http://%s:%d/_start' %
(self.server_address[0], self.server_address[1])).read()
except IOError as e:
logging.warning(e)
continue
return
def stop(self):
self._should_run = False
try:
# Load a url so |handle_request| returns.
urllib.request.urlopen(
'http://%s:%d/_stop' %
(self.server_address[0], self.server_address[1])).read()
except IOError as e:
logging.warning(e)
self._thread.join()
self._thread = None
def split_cts_expectations_and_web_test_expectations(
expectations_file_contents, platform_tags=None):
"""Split web test expectations (bit.ly/chromium-test-list-format) into a Javascript
module containing expectations for the WebGPU CTS, and a filtered list of the same web
test expectations, excluding the bits handled by the WebGPU CTS. Returns an object:
{
cts_expectations_js: "export const expectations = [ ... ]",
web_test_expectations: {
expectations: <expectations contents>
tag_set: <frozenset of tags used by the expectations>
result_set: <frozenset of result tags used by the expectations>
}
}"""
cts_expectations = []
out_tag_set = set()
out_result_set = set()
out_expectations = []
parser = TaggedTestListParser(expectations_file_contents)
# For each expectation, append it to |cts_expectations| if the CTS can understand it.
# Expectations not supported by the CTS will be forwarded to the web tests harness.
# This allows us to preserve expectations like [ Slow Crash Timeout RetryOnFailure ].
# It also preserves expectations like [ Pass ] which are used for test splitting.
# TODO(crbug.com/1186320): Handle test splits / variant generation separately?
# Web test expectations that are passed through are run as separate variants.
# Since [ Slow Crash Timeout RetryOnFailure Pass ] are Web test expectations,
# they have the downside that they must be a prefix of the test name. If they don't match
# anything the variant generator will warn.
# TODO(crbug.com/1186320): Also validate the CTS expectation query.
# TODO(crbug.com/1186320): We may be able to use skip expectations in the
# CTS for Crash/Timeout, and then have a separate test suite which runs only the problematic
# tests. We would generate variants specifically for each expectation to avoid the
# prefix problem. This would allow us to have exact test suppressions at the cost of
# potentially running some tests multiple times if there are overlapping expectations.
for exp in parser.expectations:
# Skip expectations that are not relevant to this platform
if platform_tags is not None and not exp.tags.issubset(platform_tags):
continue
results = exp.results
raw_results = exp.raw_results
# Do not do special handling of expectations that aren't for the CTS.
# ex.) ref tests run in WPT without the CTS.
# TODO(crbug.com/1186320): This could be a more robust check.
if 'q=webgpu:' in exp.test:
# Pass Skip expectations to the CTS.
if ResultType.Skip in results:
assert len(
results
) == 1, 'Skip expectations must not be combined with other expectations'
cts_expectations.append({
'query': exp.test,
'expectation': 'skip'
})
continue
# Consume the [ Failure ] expectation for the CTS, but forward along other expectations.
# [ Pass, Crash, Timeout ] will impact variant generation.
# TODO(crbug.com/1186320): Teach the CTS RetryOnFailure.
if ResultType.Failure in results and not exp.should_retry_on_failure:
cts_expectations.append({
'query': exp.test,
'expectation': 'fail'
})
results = results.difference(set((ResultType.Failure, )))
raw_results = [r for r in raw_results if r != 'Failure']
if len(raw_results) != 0:
# Forward everything, with the modified results.
out_exp = Expectation(reason=exp.reason,
test=exp.test,
results=results,
lineno=exp.lineno,
retry_on_failure=exp.should_retry_on_failure,
is_slow_test=exp.is_slow_test,
conflict_resolution=exp.conflict_resolution,
raw_tags=exp.raw_tags,
raw_results=raw_results,
is_glob=exp.is_glob,
trailing_comments=exp.trailing_comments)
out_expectations.append(out_exp)
# Add the results and tags the expectation uses to sets.
# We will prepend these to the top of the out file.
out_result_set = out_result_set.union(out_exp.raw_results)
out_tag_set = out_tag_set.union(out_exp.raw_tags)
return {
'cts_expectations_js':
'export const expectations = ' + json.dumps(cts_expectations),
'web_test_expectations': {
'expectations': out_expectations,
'tag_set': out_tag_set,
'result_set': out_result_set
}
}
def main(args, stderr):
parser = argparse.ArgumentParser(
description=
'Start the WebGPU expectations server, then forwards to run_web_tests.py'
)
parser.add_argument('--webgpu-cts-expectations', required=True)
options, rest_args = parser.parse_known_args(args)
web_test_expectations_fd, web_test_expectations_file = mkstemp()
forwarded_args = rest_args + [
'--ignore-default-expectations', '--additional-expectations',
web_test_expectations_file
]
run_web_tests_options = run_web_tests.parse_args(forwarded_args)[0]
# Construct a web tests port using the test arguments forwarded to run_web_tests.py
# (ex. --platform=android) in order to discover the tags that the web tests harness will
# use. This includes the OS, OS version, architecture, etc.
platform_tags = Host().port_factory.get(
run_web_tests_options.platform,
run_web_tests_options).get_platform_tags()
with open(options.webgpu_cts_expectations) as f:
split_result = split_cts_expectations_and_web_test_expectations(
f.read(), platform_tags)
# Write the out expectation file for web tests.
with open(web_test_expectations_file, 'w') as expectations_out:
web_test_exp = split_result['web_test_expectations']
expectations_out.write('# tags: [ ' +
' '.join(web_test_exp['tag_set']) + ' ]\n')
expectations_out.write('# results: [ Slow ' +
' '.join(web_test_exp['result_set']) + ' ]\n\n')
for exp in web_test_exp['expectations']:
expectations_out.write(exp.to_string() + '\n')
server = ExpectationsServer(split_result['cts_expectations_js'],
('127.0.0.1', 3000))
logging.info('Starting expectations server...')
server.start()
try:
return run_web_tests.main(forwarded_args, stderr)
finally:
logging.info('Stopping expectations server...')
server.stop()
os.close(web_test_expectations_fd)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:], sys.stderr))