forked from LeenkxTeam/Kmake
392 lines
14 KiB
Python
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)
|