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
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.
- Update the tools/cts_config/webview_cts_gcs_path.json file with origin,
and filenames for each platform. See: for the latest
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.
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))
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))
def create_cipd_cmd(self):
"""Performs the create-cipd sub-command."""
print('Updating WebView CTS package in CIPD.')
def update_repository_cmd(self):
"""Performs the update-checkout sub-command."""
print('Updating current checkout with changes.')
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/.
platforms: List of platforms (e.g. ['O', 'P']), defaults to all
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:
origin = self._CTSConfig.get_origin(p, a)
destination = os.path.join(self._download_dir,
self._CTSConfig.get_origin_zip(p, a))'Starting download from %s to %s.', origin, destination)
threads.append((origin,, destination)))
for t in threads:
t[1].join()'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.
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)
with cts_utils.chdir(self._download_dir):
downloads = os.listdir('.')
for download in downloads:'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.
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()'Download current CIPD version %s to %s/', version,
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.
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)
filtered = os.listdir(self._filter_dir)
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)):
if origin_base in filtered:'Staging downloaded and filtered version of %s to %s.',
origin_base, dest_path)
['cp', os.path.join(self._filter_dir, origin_base), dest_path])
else:'Staging reused %s to %s/',
os.path.join(self._cipd_dir, cipd_zip), dest_path)
['cp', os.path.join(self._cipd_dir, cipd_zip), dest_path])
os.path.join(self._stage_dir, self._CIPDYaml.get_file_basename()))
def commit_staged_cipd(self):
"""Upload the staged CIPD files to CIPD.
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,
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(
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')'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:'Saving new CIPD version %s to %s', 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.
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,
if not os.path.exists(staged_yaml_path):
raise MissingFileError(staged_yaml_path)
with open(self._version_file) as vf:
new_cipd_version ='Read in new CIPD version %s from %s', new_cipd_version,
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()))'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)'Regenerate buildbot json files under %s.', self._repo_root)
self._repo_helper.update_testing_json()'Copy staged %s to %s.', staged_yaml_path, repo_cipd_yaml)
cmd_helper.RunCmd(['cp', staged_yaml_path, repo_cipd_yaml])'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,
DESC = """Updates the WebView CTS tests to a new version.
See for the latest
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.
parser: The parser object to add to
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.
parser: The parser object to add to
is_required: Is this a required argument
help='Use this directory for'
' intermediate files.')
def main():
parser = argparse.ArgumentParser(
description=DESC, formatter_class=argparse.RawTextHelpFormatter)
subparsers = parser.add_subparsers(dest='cmd')
all_subparser = subparsers.add_parser(
help='Performs all other sub-commands, in the correct order. This is'
' usually what you want.')
add_workdir_arg(all_subparser, False)
download_subparser = subparsers.add_parser(
help='Only downloads files to workdir for later use by other'
' sub-commands.')
add_workdir_arg(download_subparser, True)
cipd_subparser = subparsers.add_parser(
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(
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()
temp_workdir = None
if args.workdir is None:
temp_workdir = tempfile.mkdtemp()
workdir = temp_workdir
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))
cts_updater = UpdateCTS(work_dir=workdir, repo_root=cts_utils.SRC_DIR)
if args.cmd == DOWNLOAD_CMD:
elif args.cmd == CIPD_UPDATE_CMD:
elif args.cmd == CHECKOUT_UPDATE_CMD:
elif args.cmd == ALL_CMD:
if temp_workdir is not None:'Removing temporary workdir %s', temp_workdir)
if __name__ == '__main__':