blob: 3a7034ea0e1e0833a2120f99c0fed71e75868478 [file] [log] [blame]
#!/usr/bin/env vpython
# Copyright 2020 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.
"""Update CTS Tests to a new version."""
from __future__ import print_function
import argparse
import logging
import os
import shutil
import sys
import tempfile
import zipfile
sys.path.append(
os.path.join(
os.path.dirname(__file__), os.pardir, os.pardir, 'third_party',
'catapult', 'devil'))
from devil.utils import cmd_helper
from devil.utils import logging_common
import cts_utils
class PathError(IOError):
def __init__(self, path, err_desc):
super(PathError, self).__init__('"%s": %s' % (path, err_desc))
class MissingDirError(PathError):
"""An expected directory is missing, usually indicates a step was missed
during the CTS update process. Try to perform the missing step.
"""
def __init__(self, path):
super(MissingDirError, self).__init__(path, 'directory is missing.')
class DirExistsError(PathError):
"""A directory is already present, usually indicates a step was repeated
in the same working directory. Try to delete the reported directory.
"""
def __init__(self, path):
super(DirExistsError, self).__init__(path, 'directory already exists.')
class MissingFileError(PathError):
"""Files are missing during CIPD staging, ensure that all files were
downloaded and that CIPD download worked properly.
"""
def __init__(self, path):
super(MissingFileError, self).__init__(path, 'file is missing.')
class InconsistentFilesException(Exception):
"""Test files in CTS config and cipd yaml have gotten out of sync."""
class UncommittedChangeException(Exception):
"""Files are about to be modified but previously uncommitted changes exist."""
def __init__(self, path):
super(UncommittedChangeException, self).__init__(
path, 'has uncommitted changes.')
class UpdateCTS(object):
"""Updates CTS archive to a new version.
Prereqs:
- Update the tools/cts_config/webview_cts_gcs_path.json file with origin,
and filenames for each platform. See:
https://source.android.com/compatibility/cts/downloads for the latest
versions.
Performs the following tasks to simplify the CTS test update process:
- Read the desired CTS versions from
tools/cts_config/webview_cts_gcs_path.json file.
- Download CTS test zip files from Android's public repository.
- Filter only the WebView CTS test apks into smaller zip files.
- Update the CTS CIPD package with the filtered zip files.
- Update DEPS and testing/buildbot/test_suites.pyl with updated CTS CIPD
package version.
- Regenerate the buildbot json files.
After these steps are completed, the user can commit and upload
the CL to Chromium Gerrit.
"""
def __init__(self, work_dir, repo_root):
"""Construct UpdateCTS instance.
Args:
work_dir: Directory used to download and stage cipd updates
repo_root: Repository root (e.g. /path/to/chromium/src) to base
all configuration files
"""
self._work_dir = os.path.abspath(work_dir)
self._download_dir = os.path.join(self._work_dir, 'downloaded')
self._filter_dir = os.path.join(self._work_dir, 'filtered')
self._cipd_dir = os.path.join(self._work_dir, 'cipd')
self._stage_dir = os.path.join(self._work_dir, 'staged')
self._version_file = os.path.join(self._work_dir, 'cipd_version.txt')
self._repo_root = os.path.abspath(repo_root)
helper = cts_utils.ChromiumRepoHelper(self._repo_root)
self._repo_helper = helper
self._CTSConfig = cts_utils.CTSConfig(
helper.rebase(cts_utils.TOOLS_DIR, cts_utils.CONFIG_FILE))
self._CIPDYaml = cts_utils.CTSCIPDYaml(
helper.rebase(cts_utils.TOOLS_DIR, cts_utils.CIPD_FILE))
@property
def download_dir(self):
"""Full directory path where full test zips are to be downloaded to."""
return self._download_dir
def download_cts_cmd(self, platforms=None):
"""Performs the download sub-command."""
if platforms is None:
all_platforms = self._CTSConfig.get_platforms()
platforms = list(
set(all_platforms) - set(cts_utils.END_OF_SERVICE_DESSERTS))
print('Downloading CTS tests for %d platforms, could take a few'
' minutes ...' % len(platforms))
self.download_cts(platforms)
def create_cipd_cmd(self):
"""Performs the create-cipd sub-command."""
print('Updating WebView CTS package in CIPD.')
self.filter_downloaded_cts()
self.download_cipd()
self.stage_cipd_update()
self.commit_staged_cipd()
def update_repository_cmd(self):
"""Performs the update-checkout sub-command."""
print('Updating current checkout with changes.')
self.update_repository()
def download_cts(self, platforms=None):
"""Download full test zip files to <work_dir>/downloaded/.
It is an error to call this if work_dir already contains downloaded/.
Args:
platforms: List of platforms (e.g. ['O', 'P']), defaults to all
Raises:
DirExistsError: If downloaded/ already exists in work_dir.
"""
if platforms is None:
platforms = self._CTSConfig.get_platforms()
if os.path.exists(self._download_dir):
raise DirExistsError(self._download_dir)
threads = []
for p, a in self._CTSConfig.iter_platform_archs():
if p not in platforms:
continue
origin = self._CTSConfig.get_origin(p, a)
destination = os.path.join(self._download_dir,
self._CTSConfig.get_origin_zip(p, a))
logging.info('Starting download from %s to %s.', origin, destination)
threads.append((origin, cts_utils.download(origin, destination)))
for t in threads:
t[1].join()
logging.info('Finished download from ' + t[0])
def filter_downloaded_cts(self):
"""Filter files from downloaded/ to filtered/ to contain only WebView apks.
It is an error to call this if downloaded/ doesn't exist or if filtered/
already exists.
Raises:
DirExistsError: If filtered/ already exists in work_dir.
MissingDirError: If downloaded/ does not exist in work_dir.
"""
if os.path.exists(self._filter_dir):
raise DirExistsError(self._filter_dir)
if not os.path.isdir(self._download_dir):
raise MissingDirError(self._download_dir)
os.makedirs(self._filter_dir)
with cts_utils.chdir(self._download_dir):
downloads = os.listdir('.')
for download in downloads:
logging.info('Filtering %s to %s/', download, self._filter_dir)
cts_utils.filter_cts_file(self._CTSConfig, download, self._filter_dir)
def download_cipd(self):
"""Download cts archive of the version found in DEPS to cipd/ directory.
It is an error to call this if cipd/ already exists under work_cir.
Raises:
DirExistsError: If cipd/ already exists in work_dir.
"""
if os.path.exists(self._cipd_dir):
raise DirExistsError(self._cipd_dir)
version = self._repo_helper.get_cipd_dependency_rev()
logging.info('Download current CIPD version %s to %s/', version,
self._cipd_dir)
cts_utils.cipd_download(self._CIPDYaml, version, self._cipd_dir)
def stage_cipd_update(self):
"""Stage CIPD package for update by combining CIPD and filtered CTS files.
It is an error to call this if filtered/ and cipd/ do not already exist
under work_dir, or if staged already exists under work_dir.
Raises:
DirExistsError: If staged/ already exists in work_dir.
MissingDirError: If filtered/ or cipd/ does not exist in work_dir.
"""
if not os.path.isdir(self._filter_dir):
raise MissingDirError(self._filter_dir)
if not os.path.isdir(self._cipd_dir):
raise MissingDirError(self._cipd_dir)
if os.path.isdir(self._stage_dir):
raise DirExistsError(self._stage_dir)
os.makedirs(self._stage_dir)
filtered = os.listdir(self._filter_dir)
self._CIPDYaml.clear_files()
for p, a in self._CTSConfig.iter_platform_archs():
origin_base = self._CTSConfig.get_origin_zip(p, a)
cipd_zip = self._CTSConfig.get_cipd_zip(p, a)
dest_path = os.path.join(self._stage_dir, cipd_zip)
if not os.path.isdir(os.path.dirname(dest_path)):
os.makedirs(os.path.dirname(dest_path))
self._CIPDYaml.append_file(cipd_zip)
if origin_base in filtered:
logging.info('Staging downloaded and filtered version of %s to %s.',
origin_base, dest_path)
cmd_helper.RunCmd(
['cp', os.path.join(self._filter_dir, origin_base), dest_path])
else:
logging.info('Staging reused %s to %s/',
os.path.join(self._cipd_dir, cipd_zip), dest_path)
cmd_helper.RunCmd(
['cp', os.path.join(self._cipd_dir, cipd_zip), dest_path])
self._CIPDYaml.write(
os.path.join(self._stage_dir, self._CIPDYaml.get_file_basename()))
def commit_staged_cipd(self):
"""Upload the staged CIPD files to CIPD.
Raises:
MissingDirError: If staged/ does not exist in work_dir.
InconsistentFilesException: If errors are detected in staged config files.
MissingFileExcepition: If files are missing from CTS zip files.
"""
if not os.path.isdir(self._stage_dir):
raise MissingDirError(self._stage_dir)
staged_yaml_path = os.path.join(self._stage_dir,
self._CIPDYaml.get_file_basename())
staged_yaml = cts_utils.CTSCIPDYaml(file_path=staged_yaml_path)
staged_yaml_files = staged_yaml.get_files()
if cts_utils.CTS_DEP_PACKAGE != staged_yaml.get_package():
raise InconsistentFilesException('Bad CTS package name in staged yaml '
'{}: {} '.format(
staged_yaml_path,
staged_yaml.get_package()))
for p, a in self._CTSConfig.iter_platform_archs():
cipd_zip = self._CTSConfig.get_cipd_zip(p, a)
cipd_zip_path = os.path.join(self._stage_dir, cipd_zip)
if not os.path.exists(cipd_zip_path):
raise MissingFileError(cipd_zip_path)
with zipfile.ZipFile(cipd_zip_path) as zf:
cipd_zip_contents = zf.namelist()
missing_apks = set(self._CTSConfig.get_apks(p)) - set(cipd_zip_contents)
if missing_apks:
raise MissingFileError('%s in %s' % (str(missing_apks), cipd_zip_path))
if cipd_zip not in staged_yaml_files:
raise InconsistentFilesException(cipd_zip +
' missing from staged cipd.yaml file')
logging.info('Updating CIPD CTS version using %s', staged_yaml_path)
new_cipd_version = cts_utils.update_cipd_package(staged_yaml_path)
with open(self._version_file, 'w') as vf:
logging.info('Saving new CIPD version %s to %s', new_cipd_version,
vf.name)
vf.write(new_cipd_version)
def update_repository(self):
"""Update chromium checkout with changes for this update.
After this is called, git add -u && git commit && git cl upload
will still be needed to generate the CL.
Raises:
MissingFileError: If CIPD has not yet been staged or updated.
UncommittedChangeException: If repo files have uncommitted changes.
InconsistentFilesException: If errors are detected in staged config files.
"""
if not os.path.exists(self._version_file):
raise MissingFileError(self._version_file)
staged_yaml_path = os.path.join(self._stage_dir,
self._CIPDYaml.get_file_basename())
if not os.path.exists(staged_yaml_path):
raise MissingFileError(staged_yaml_path)
with open(self._version_file) as vf:
new_cipd_version = vf.read()
logging.info('Read in new CIPD version %s from %s', new_cipd_version,
vf.name)
repo_cipd_yaml = self._CIPDYaml.get_file_path()
for f in self._repo_helper.cipd_referrers + [repo_cipd_yaml]:
git_status = self._repo_helper.git_status(f)
if git_status:
raise UncommittedChangeException(f)
repo_cipd_package = self._repo_helper.cts_cipd_package
staged_yaml = cts_utils.CTSCIPDYaml(file_path=staged_yaml_path)
if repo_cipd_package != staged_yaml.get_package():
raise InconsistentFilesException(
'Inconsistent CTS package name, {} in {}, but {} in {}'.format(
repo_cipd_package, cts_utils.DEPS_FILE, staged_yaml.get_package(),
staged_yaml.get_file_path()))
logging.info('Updating files that reference %s under %s.',
cts_utils.CTS_DEP_PACKAGE, self._repo_root)
self._repo_helper.update_cts_cipd_rev(new_cipd_version)
logging.info('Regenerate buildbot json files under %s.', self._repo_root)
self._repo_helper.update_testing_json()
logging.info('Copy staged %s to %s.', staged_yaml_path, repo_cipd_yaml)
cmd_helper.RunCmd(['cp', staged_yaml_path, repo_cipd_yaml])
logging.info('Ensure CIPD CTS package at %s to the new version %s',
repo_cipd_yaml, new_cipd_version)
cts_utils.cipd_ensure(self._CIPDYaml.get_package(), new_cipd_version,
os.path.dirname(repo_cipd_yaml))
DESC = """Updates the WebView CTS tests to a new version.
See https://source.android.com/compatibility/cts/downloads for the latest
versions.
Please create a new branch, then edit the
{}
file with updated origin and file name before running this script.
After performing all steps, perform git add then commit.""".format(
os.path.join(cts_utils.TOOLS_DIR, cts_utils.CONFIG_FILE))
ALL_CMD = 'all-steps'
DOWNLOAD_CMD = 'download'
CIPD_UPDATE_CMD = 'create-cipd'
CHECKOUT_UPDATE_CMD = 'update-checkout'
def add_dessert_arg(parser):
"""Add --dessert argument to a parser.
Args:
parser: The parser object to add to
"""
parser.add_argument(
'--dessert',
'-d',
action='append',
help='Android dessert letter(s) for which to perform CTS update.')
def add_workdir_arg(parser, is_required):
"""Add --work-dir argument to a parser.
Args:
parser: The parser object to add to
is_required: Is this a required argument
"""
parser.add_argument(
'--workdir',
'-w',
required=is_required,
help='Use this directory for'
' intermediate files.')
def main():
parser = argparse.ArgumentParser(
description=DESC, formatter_class=argparse.RawTextHelpFormatter)
logging_common.AddLoggingArguments(parser)
subparsers = parser.add_subparsers(dest='cmd')
all_subparser = subparsers.add_parser(
ALL_CMD,
help='Performs all other sub-commands, in the correct order. This is'
' usually what you want.')
add_dessert_arg(all_subparser)
add_workdir_arg(all_subparser, False)
download_subparser = subparsers.add_parser(
DOWNLOAD_CMD,
help='Only downloads files to workdir for later use by other'
' sub-commands.')
add_dessert_arg(download_subparser)
add_workdir_arg(download_subparser, True)
cipd_subparser = subparsers.add_parser(
CIPD_UPDATE_CMD,
help='Create a new CIPD package version for CTS tests. This requires'
' that {} was completed in the same workdir.'.format(DOWNLOAD_CMD))
add_workdir_arg(cipd_subparser, True)
checkout_subparser = subparsers.add_parser(
CHECKOUT_UPDATE_CMD,
help='Updates files in the current git branch. This requires that {} was'
' completed in the same workdir.'.format(CIPD_UPDATE_CMD))
add_workdir_arg(checkout_subparser, True)
args = parser.parse_args()
logging_common.InitializeLogging(args)
temp_workdir = None
if args.workdir is None:
temp_workdir = tempfile.mkdtemp()
workdir = temp_workdir
else:
workdir = args.workdir
if not os.path.isdir(workdir):
raise ValueError(
'--workdir {} should already be a directory.'.format(workdir))
if not os.access(workdir, os.W_OK | os.X_OK):
raise ValueError('--workdir {} is not writable.'.format(workdir))
try:
cts_updater = UpdateCTS(work_dir=workdir, repo_root=cts_utils.SRC_DIR)
if args.cmd == DOWNLOAD_CMD:
cts_updater.download_cts_cmd(platforms=args.dessert)
elif args.cmd == CIPD_UPDATE_CMD:
cts_updater.create_cipd_cmd()
elif args.cmd == CHECKOUT_UPDATE_CMD:
cts_updater.update_repository_cmd()
elif args.cmd == ALL_CMD:
cts_updater.download_cts_cmd()
cts_updater.create_cipd_cmd()
cts_updater.update_repository_cmd()
finally:
if temp_workdir is not None:
logging.info('Removing temporary workdir %s', temp_workdir)
shutil.rmtree(temp_workdir)
if __name__ == '__main__':
main()