Files
Kmake/deps/v8/test/test262/tools/v8_importer.py
2026-05-26 23:36:42 -07:00

392 lines
14 KiB
Python

# Copyright 2023 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from abc import ABC, abstractmethod
from enum import Enum
from pathlib import Path
import argparse
import logging
import re
import shutil
import sys
from blinkpy.common.system.log_utils import configure_logging
from blinkpy.w3c.common import read_credentials
TEST_FILE_REFERENCE_IN_STATUS_FILE = re.compile("^\s*'(.*)':.*,$")
TEST262_FAILURE_LINE = re.compile("=== test262/(.*) ===")
TEST262_PATTERN = re.compile('^test/(.*)\.[^\s]*$')
TEST262_RENAME_PATTERN = re.compile('^R[^\s]*\s*([^\s]*)\s*([^\s]*)$')
TEST262_REPO_URL = 'https://chromium.googlesource.com/external/github.com/tc39/test262'
V8_TEST262_ROLLS_META_BUG = 'v8:7834'
_log = logging.getLogger(__file__)
class GitFileStatus(Enum):
ADDED = 'A'
DELETED = 'D'
MODIFIED = 'M'
UNKNOWN = 'X'
class V8TestImporter():
def __init__(self,
phase,
host,
test262_github=None,
test262_failure_file=None,
v8_test262_last_rev=None):
self.host = host
self.github = test262_github
self.project_config = host.project_config
self.project_root = Path(self.project_config.project_root)
self.test262_status_file = self.project_root / 'test' / 'test262' / 'test262.status'
self.phase = phase
self.test262_failure_file = test262_failure_file
self.v8_test262_last_rev = v8_test262_last_rev
def parse_args(self, argv):
parser = argparse.ArgumentParser()
parser.description = __doc__
parser.add_argument(
'-v',
'--verbose',
action='store_true',
help='log extra details that may be helpful when debugging')
parser.add_argument(
'--credentials-json',
help='A JSON file with GitHub credentials, '
'generally not necessary on developer machines')
return parser.parse_args(argv)
def main(self, argv=None):
options = self.parse_args(argv)
self.verbose = options.verbose
log_level = logging.DEBUG if self.verbose else logging.INFO
configure_logging(logging_level=log_level, include_time=True)
credentials = read_credentials(self.host, options.credentials_json)
gh_user = credentials.get('GH_USER')
gh_token = credentials.get('GH_TOKEN')
if not gh_user or not gh_token:
_log.warning('You have not set your GitHub credentials. This '
'script may fail with a network error when making '
'an API request to GitHub.')
_log.warning('See https://chromium.googlesource.com/chromium/src'
'/+/main/docs/testing/web_platform_tests.md'
'#GitHub-credentials for instructions on how to set '
'your credentials up.')
self.github = self.project_config.github_factory(self.host, gh_user,
gh_token)
test262_rev = self.fetch_test262(gh_token)
v8_test262_rev = (
self.v8_test262_last_rev or self.find_current_test262_rev())
if self.run_prebuild_phase():
if test262_rev == v8_test262_rev:
_log.info(f'No changes to import. {test262_rev} == {v8_test262_rev}')
return False
_log.info(f'Importing test262@{test262_rev} to V8')
self.roll_as_dependency(test262_rev)
self.overwrite_files()
self.sync_folders(v8_test262_rev, test262_rev)
failure_lines = []
if self.run_build_phase():
failure_lines = self.build_and_test()
# We either have the lines from the build phase or we read them from a file
# provided by the executor of POSTBUILD phase.
failure_lines = failure_lines or self.failure_lines_from_file()
if self.run_postbuild_phase():
self.update_status_file(v8_test262_rev, test262_rev, failure_lines)
# Output the import range for the recipe to pick up.
print(f'{TEST262_REPO_URL}/+log/{v8_test262_rev[:8]}..{test262_rev[:8]}')
if self.run_upload_phase():
self.commit_and_upload_changes(v8_test262_rev, test262_rev)
# TODO: Create bug if status update yields a skip block
return True
@property
def test262_path(self):
return Path(self.local_test262.path)
def run_prebuild_phase(self):
return self.phase in ['ALL', 'PREBUILD']
def run_build_phase(self):
return self.phase in ['ALL']
def run_postbuild_phase(self):
return self.phase in ['ALL', 'POSTBUILD']
def run_upload_phase(self):
return self.phase in ['ALL', 'UPLOAD']
def fetch_test262(self, gh_token):
_log.info(f'Fetching test262')
self.local_test262 = self.project_config.local_repo_factory(
self.host, gh_token=gh_token)
self.local_test262.fetch()
self.test262_git = self.host.git(self.test262_path)
return self.test262_git.latest_git_commit()
def find_current_test262_rev(self):
_log.info(f'Finding current test262 revision in V8')
return self.host.executive.run_command(
['gclient', 'getdep', '-r', 'test/test262/data'],
cwd=self.project_root).splitlines()[-1].strip()
def roll_as_dependency(self, test262_rev):
self.host.executive.run_command(
['gclient', 'setdep', '-r', f'test/test262/data@{test262_rev}'],
cwd=self.project_root)
def overwrite_files(self):
for file in self.project_config.files_to_copy:
_log.info(f'Overwriting {file.destination} with {file.source}')
shutil.copyfile(
Path(self.local_test262.path) / file.source,
self.project_root / file.destination)
def sync_folders(self, v8_test262_rev, test262_rev):
for folder in self.project_config.paths_to_sync:
_log.info(f'Sync {folder.destination} with {folder.source}')
destination = self.project_root / folder.destination
for f in destination.glob('./**/*'):
if f.is_file() and not self.is_git_sync_exception(f):
relative_path = f.relative_to(destination)
source_file = self.test262_path / folder.source / relative_path
status = self.get_git_file_status(v8_test262_rev, test262_rev,
source_file)
self.update_file(f, status, source_file)
def is_git_sync_exception(self, file):
return file.name == 'features.txt'
def get_git_file_status(self, v8_test262_rev, test262_rev, source_file):
status_line = self.test262_git.run([
'diff', '--name-status', v8_test262_rev, test262_rev, '--', source_file
]).splitlines()
assert len(status_line) < 2, f'Expected zero or one line, got {status_line}'
if len(status_line) == 0:
return GitFileStatus.UNKNOWN
return GitFileStatus(status_line[0][0])
def update_file(self, local_file, status, source_file):
gfs = GitFileStatus
if status in [gfs.ADDED, gfs.DELETED, gfs.MODIFIED]:
_log.info(f'{local_file} has counterpart in Test262. Deleting.')
local_file.unlink()
else:
_log.warning(
f'{local_file} has no counterpart in Test262. '
'Maybe it was never exported?'
)
def build_and_test(self):
"""Builds and run test262 tests V8."""
_log.info('Building V8')
build_config = 'x64.release'
self.host.executive.run_command(
[sys.executable, 'tools/dev/gm.py', build_config],
cwd=self.project_root)
_log.info('Running test262 tests')
test_results = self.host.executive.run_command(
[
sys.executable, 'tools/run-tests.py',
f'--outdir=out/{build_config}', '--progress=verbose',
'--exit-after-n-failures=0', 'test262'
],
error_handler=testing_error_handler,
cwd=self.project_root).splitlines()
failure_matches = [TEST262_FAILURE_LINE.match(l) for l in test_results]
return uniq(m.group(1) for m in failure_matches if m)
def failure_lines_from_file(self):
if not self.test262_failure_file:
_log.warning('No failure file provided. Skipping.')
return []
with open(self.test262_failure_file, 'r') as r_file:
return r_file.readlines()
def update_status_file(self, v8_test262_rev, test262_rev, failure_lines):
_log.info(f'Updating status file')
updated_status = self.remove_and_rename(v8_test262_rev, test262_rev)
added_lines = self.failed_tests_to_status_lines(failure_lines)
if added_lines:
updated_status = self.rewrite_status_file_content(updated_status,
added_lines,
v8_test262_rev,
test262_rev)
with open(self.test262_status_file, 'w') as w_file:
w_file.writelines(updated_status)
def remove_and_rename(self, v8_test262_rev, test262_rev):
remover = DeletedTestsRemover(self, v8_test262_rev, test262_rev)
renamer = RenamedTestsUpdater(self, v8_test262_rev, test262_rev)
def updated_line(line):
return remover.updated_line(renamer.updated_line(line))
updated_status = [updated_line(line) for line in self.status_file_lines()]
return [line for line in updated_status if line is not None]
def status_file_lines(self):
with open(self.test262_status_file, 'r') as r_file:
return r_file.readlines()
def failed_tests_to_status_lines(self, failed_tests):
# Transform the list of failed tests into a list of status file lines.
return [f" '{test}': [FAIL],\n" for test in failed_tests]
def rewrite_status_file_content(self, updated_status, added_lines,
v8_test262_rev, test262_rev):
# Reassemble the status file with the new tests added.
# TODO(liviurau): This is easy to unit test. Add unit tests.
status_lines_before_eof = updated_status[:-2]
eof_status_lines = updated_status[-2:]
assert eof_status_lines == [
'\n', ']\n'
], f'Unexpected status file eof. {eof_status_lines}'
import_header_lines = [
'\n####\n', f'# Import test262@{test262_rev[:8]}\n',
f'# {TEST262_REPO_URL}/+log/{v8_test262_rev[:8]}..{test262_rev[:8]}\n'
]
new_failing_tests_lines = ['[ALWAYS, {\n'] + added_lines + [
'}],\n', f'# End import test262@{test262_rev[:8]}\n', '####\n'
]
return (status_lines_before_eof + import_header_lines +
new_failing_tests_lines + eof_status_lines)
def commit_and_upload_changes(self, v8_test262_rev, test262_rev):
_log.info('Committing changes.')
self.project_git.run([
'commit',
'-a',
'-m',
'[test262] Roll test262',
'-m',
f'{TEST262_REPO_URL}/+log/{v8_test262_rev[:8]}..{test262_rev[:8]}',
'-m',
'no-export: true',
])
_log.info('Uploading changes.')
self.project_git.run([
'cl',
'upload',
'--bypass-hooks',
'-f',
'-b',
V8_TEST262_ROLLS_META_BUG,
'-d',
])
_log.info(f'Issue: {self.project_git.run(["cl", "issue"]).strip()}')
def uniq(lst):
"""Return a list with unique elements from the input list."""
return sorted(set(lst))
def testing_error_handler(error):
"""Error handler that does nothing; used to suppress errors."""
pass
def relative_test_name(name):
"""Remove the test262 prefix from the test name."""
return re.sub(TEST262_PATTERN, r'\1', name)
class StatusFileUpdater(ABC):
def __init__(self, importer, v8_test262_rev, test262_rev, diff_args):
self.importer = importer
self.diff_args = diff_args
self.to_update = self.collect_updateable_tests(v8_test262_rev, test262_rev)
def updated_line(self, line):
test_name_result = TEST_FILE_REFERENCE_IN_STATUS_FILE.match(line)
if test_name_result:
return self.update_test_line(line, test_name_result)
return line
def collect_updateable_tests(self, v8_test262_rev, test262_rev):
lines = self.importer.test262_git.run([
'diff', *self.diff_args, v8_test262_rev, test262_rev, '--', 'test'
]).splitlines()
return self.process_git_output(lines)
@abstractmethod
def process_git_output(self, lines):
pass
@abstractmethod
def update_test_line(self, line, test_name_result):
pass
class DeletedTestsRemover(StatusFileUpdater):
def __init__(self, importer, v8_test262_rev, test262_rev):
super().__init__(importer, v8_test262_rev, test262_rev,
['--name-only', '--diff-filter=D'])
def process_git_output(self, lines):
return [
relative_test_name(line)
for line in lines
if TEST262_PATTERN.match(line)
]
def update_test_line(self, line, test_name_result):
test_name = test_name_result.group(1)
if test_name not in self.to_update:
return line
_log.info(f'... removing {test_name}')
return None
class RenamedTestsUpdater(StatusFileUpdater):
def __init__(self, importer, v8_test262_rev, test262_rev):
super().__init__(importer, v8_test262_rev, test262_rev,
['--name-status', '--diff-filter=R'])
def process_git_output(self, lines):
search_renames = [re.search(TEST262_RENAME_PATTERN, line) for line in lines]
renames = {
rename.group(1): rename.group(2) for rename in search_renames if rename
}
return {
relative_test_name(key): relative_test_name(value)
for key, value in renames.items()
if TEST262_PATTERN.match(key) and TEST262_PATTERN.match(value)
}
def update_test_line(self, line, test_name_result):
old_name = test_name_result.group(1)
new_name = self.to_update.get(old_name)
if not new_name:
return line
_log.info(f'... updating {old_name} to {new_name}')
return line.replace(old_name, new_name)