Commit 5bc47704 authored by Andrew Luo's avatar Andrew Luo Committed by Commit Bot

Automate process of updating CTS tests.

Bug: 1049431
Test: python update_cts_test.py
Change-Id: Ie99e692b094a01cbb1e2122f8865a9ad15efd44b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2035063
Commit-Queue: Andrew Luo <aluo@chromium.org>
Reviewed-by: default avatarNate Fischer <ntfschr@chromium.org>
Cr-Commit-Position: refs/heads/master@{#745731}
parent b4b69d2e
#!/usr/bin/env python
# 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.
"""Stage the Chromium checkout to update CTS test version."""
import contextlib
import json
import os
import re
import sys
import tempfile
import threading
import urllib
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
sys.path.append(
os.path.join(
os.path.dirname(__file__), os.pardir, os.pardir, 'third_party',
'catapult', 'common', 'py_utils'))
from py_utils import tempfile_ext
sys.path.append(
os.path.join(
os.path.dirname(__file__), os.pardir, os.pardir, 'third_party',
'depot_tools'))
import gclient_eval
SRC_DIR = os.path.abspath(
os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
TOOLS_DIR = os.path.join('android_webview', 'tools')
CONFIG_FILE = os.path.join('cts_config', 'webview_cts_gcs_path.json')
CONFIG_PATH = os.path.join(SRC_DIR, TOOLS_DIR, CONFIG_FILE)
CIPD_FILE = os.path.join('cts_archive', 'cipd.yaml')
CIPD_PATH = os.path.join(SRC_DIR, TOOLS_DIR, CIPD_FILE)
DEPS_FILE = 'DEPS'
TEST_SUITES_FILE = os.path.join('testing', 'buildbot', 'test_suites.pyl')
# Android desserts that are no longer receiving CTS updates at
# https://source.android.com/compatibility/cts/downloads
# Please update this list as more versions reach end-of-service.
END_OF_SERVICE_DESSERTS = ['L', 'M']
CTS_DEP_NAME = 'src/android_webview/tools/cts_archive'
CTS_DEP_PACKAGE = 'chromium/android_webview/tools/cts_archive'
CIPD_REFERRERS = [DEPS_FILE, TEST_SUITES_FILE]
_GENERATE_BUILDBOT_JSON = os.path.join('testing', 'buildbot',
'generate_buildbot_json.py')
_ENSURE_FORMAT = """$ParanoidMode CheckIntegrity
@Subdir cipd
{} {}"""
_ENSURE_SUBDIR = 'cipd'
_RE_COMMENT_OR_BLANK = re.compile(r'^ *(#.*)?$')
class CTSConfig(object):
"""Represents a CTS config file."""
def __init__(self, file_path=CONFIG_PATH):
"""Constructs a representation of the CTS config file.
Only read operations are provided by this object. Users should edit the
file manually for any modifications.
Args:
file_path: Path to file.
"""
self._path = os.path.abspath(file_path)
with open(self._path) as f:
self._config = json.load(f)
def get_platforms(self):
return sorted(self._config.keys())
def get_archs(self, platform):
return sorted(self._config[platform]['arch'].keys())
def iter_platform_archs(self):
for p in self.get_platforms():
for a in self.get_archs(p):
yield p, a
def get_cipd_zip(self, platform, arch):
return self._config[platform]['arch'][arch]['filename']
def get_origin(self, platform, arch):
return self._config[platform]['arch'][arch]['_origin']
def get_origin_zip(self, platform, arch):
return os.path.basename(self.get_origin(platform, arch))
def get_apks(self, platform):
return sorted([r['apk'] for r in self._config[platform]['test_runs']])
class CTSCIPDYaml(object):
"""Represents a CTS CIPD yaml file."""
RE_PACKAGE = r'^package:\s*(\S+)\s*$'
RE_DESC = r'^description:\s*(.+)$'
RE_DATA = r'^data:\s*$'
RE_FILE = r'^\s+-\s+file:\s*(.+)$'
# TODO(crbug.com/1049432): Replace with yaml parser
@classmethod
def parse(cls, lines):
result = {}
for line in lines:
if len(line) == 0 or line[0] == '#':
continue
package_match = re.match(cls.RE_PACKAGE, line)
if package_match:
result['package'] = package_match.group(1)
continue
desc_match = re.match(cls.RE_DESC, line)
if desc_match:
result['description'] = desc_match.group(1)
continue
if re.match(cls.RE_DATA, line):
result['data'] = []
if 'data' in result:
file_match = re.match(cls.RE_FILE, line)
if file_match:
result['data'].append({'file': file_match.group(1)})
return result
def __init__(self, file_path=CIPD_PATH):
"""Constructs a representation of CTS CIPD yaml file.
Note the file won't be modified unless write is called
with its path.
Args:
file_path: Path to file.
"""
self._path = os.path.abspath(file_path)
self._header = []
# Read header comments
with open(self._path) as f:
for l in f.readlines():
if re.match(_RE_COMMENT_OR_BLANK, l):
self._header.append(l)
else:
break
# Read yaml data
with open(self._path) as f:
self._yaml = CTSCIPDYaml.parse(f.readlines())
def get_file_path(self):
"""Get full file path of yaml file that this was constructed from."""
return self._path
def get_file_basename(self):
"""Get base file name that this was constructed from."""
return os.path.basename(self._path)
def get_package(self):
"""Get package name."""
return self._yaml['package']
def clear_files(self):
"""Clears all files in file (only in local memory, does not modify file)."""
self._yaml['data'] = []
def append_file(self, file_name):
"""Add file_name to list of files."""
self._yaml['data'].append({'file': str(file_name)})
def remove_file(self, file_name):
"""Remove file_name from list of files."""
old_file_names = self.get_files()
new_file_names = [name for name in old_file_names if name != file_name]
self._yaml['data'] = [{'file': name} for name in new_file_names]
def get_files(self):
"""Get list of files in yaml file."""
return [e['file'] for e in self._yaml['data']]
def write(self, file_path):
"""(Over)write file_path with the cipd.yaml representation."""
dir_name = os.path.dirname(file_path)
if not os.path.isdir(dir_name):
os.makedirs(dir_name)
with open(file_path, 'w') as f:
f.writelines(self._get_yamls())
def _get_yamls(self):
"""Return the cipd.yaml file contents of this object."""
output = []
output += self._header
output.append('package: {}\n'.format(self._yaml['package']))
output.append('description: {}\n'.format(self._yaml['description']))
output.append('data:\n')
self._yaml['data'].sort()
for d in self._yaml['data']:
output.append(' - file: {}\n'.format(d.get('file')))
return output
def cipd_ensure(package, version, root_dir):
"""Ensures CIPD package is installed at root_dir.
Args:
package: CIPD name of package
version: Package version
root_dir: Directory to install package into
"""
def _createEnsureFile(package, version, file_path):
with open(file_path, 'w') as f:
f.write(_ENSURE_FORMAT.format(package, version))
def _ensure(root, ensure_file):
ret = cmd_helper.RunCmd(
['cipd', 'ensure', '-root', root, '-ensure-file', ensure_file])
if ret:
raise IOError('Error while running cipd ensure: ' + ret)
with tempfile.NamedTemporaryFile() as f:
_createEnsureFile(package, version, f.name)
_ensure(root_dir, f.name)
def cipd_download(cipd, version, download_dir):
"""Downloads CIPD package files.
This is different from cipd ensure in that actual files will exist at
download_dir instead of symlinks.
Args:
cipd: CTSCIPDYaml object
version: Version of package
download_dir: Destination directory
"""
package = cipd.get_package()
download_dir_abs = os.path.abspath(download_dir)
if not os.path.isdir(download_dir_abs):
os.makedirs(download_dir_abs)
with tempfile_ext.NamedTemporaryDirectory() as workDir, chdir(workDir):
cipd_ensure(package, version, '.')
for file_name in cipd.get_files():
src_path = os.path.join(_ENSURE_SUBDIR, file_name)
dest_path = os.path.join(download_dir_abs, file_name)
dest_dir = os.path.dirname(dest_path)
if not os.path.isdir(dest_dir):
os.makedirs(dest_dir)
ret = cmd_helper.RunCmd(['cp', '--reflink=never', src_path, dest_path])
if ret:
raise IOError('Error file copy from ' + file_name + ' to ' + dest_path)
def filter_cts_file(cts_config, cts_zip_file, dest_dir):
"""Filters out non-webview test apks from downloaded CTS zip file.
Args:
cts_config: CTSConfig object
cts_zip_file: Path to downloaded CTS zip, retaining the original filename
dest_dir: Destination directory to filter to, filename will be unchanged
"""
for p in cts_config.get_platforms():
for a in cts_config.get_archs(p):
o = cts_config.get_origin(p, a)
base_name = os.path.basename(o)
if base_name == os.path.basename(cts_zip_file):
filterzip(cts_zip_file, cts_config.get_apks(p),
os.path.join(dest_dir, base_name))
return
raise ValueError('Could not find platform and arch for: ' + cts_zip_file)
class ChromiumRepoHelper(object):
"""Performs operations on Chromium checkout."""
def __init__(self, root_dir=SRC_DIR):
self._root_dir = os.path.abspath(root_dir)
self._cipd_referrers = [
os.path.join(self._root_dir, p) for p in CIPD_REFERRERS
]
@property
def cipd_referrers(self):
return self._cipd_referrers
@property
def cts_cipd_package(self):
return CTS_DEP_PACKAGE
def get_cipd_dependency_rev(self):
"""Return CTS CIPD revision in the checkout's DEPS file."""
deps_file = os.path.join(self._root_dir, DEPS_FILE)
with open(deps_file) as f:
contents = f.read()
dd = gclient_eval.Parse(contents, False, deps_file)
return gclient_eval.GetCIPD(dd, CTS_DEP_NAME, CTS_DEP_PACKAGE)
def update_cts_cipd_rev(self, new_version):
"""Update references to CTS CIPD revision in checkout.
Args:
new_version: New version to use
"""
old_version = self.get_cipd_dependency_rev()
for path in self.cipd_referrers:
replace_cipd_revision(path, old_version, new_version)
def git_status(self, path):
"""Returns canonical git status of file.
Args:
path: Path to file.
Returns:
Output of git status --porcelain.
"""
with chdir(self._root_dir):
output = cmd_helper.GetCmdOutput(['git', 'status', '--porcelain', path])
return output
def update_testing_json(self):
"""Performs generate_buildbot_json.py.
Raises:
IOError: If generation failed.
"""
with chdir(self._root_dir):
ret = cmd_helper.RunCmd(['python', _GENERATE_BUILDBOT_JSON])
if ret:
raise IOError('Error while generating_buildbot_json.py')
def rebase(self, *rel_path_parts):
"""Construct absolute path from parts relative to root_dir.
Args:
rel_path_parts: Parts of the root relative path.
Returns:
The absolute path.
"""
return os.path.join(self._root_dir, *rel_path_parts)
def replace_cipd_revision(file_path, old_revision, new_revision):
"""Replaces cipd revision strings in file.
Args:
file_path: Path to file.
old_revision: Old cipd revision to be replaced.
new_revision: New cipd revision to use as replacement.
Returns:
Number of replaced occurrences.
Raises:
IOError: If no occurrences were found.
"""
with open(file_path) as f:
contents = f.read()
num = contents.count(old_revision)
if not num:
raise IOError('Did not find old CIPD revision {} in {}'.format(
old_revision, file_path))
newcontents = contents.replace(old_revision, new_revision)
with open(file_path, 'w') as f:
f.write(newcontents)
return num
@contextlib.contextmanager
def chdir(dirPath):
"""Context manager that changes working directory."""
cwd = os.getcwd()
os.chdir(dirPath)
try:
yield
finally:
os.chdir(cwd)
def filterzip(inputPath, pathList, outputPath):
"""Copy a subset of files from input archive into output archive.
Args:
inputPath: Input archive path
pathList: List of file names from input archive to copy
outputPath: Output archive path
"""
with zipfile.ZipFile(os.path.abspath(inputPath), 'r') as inputZip,\
zipfile.ZipFile(os.path.abspath(outputPath), 'w') as outputZip,\
tempfile_ext.NamedTemporaryDirectory() as workDir,\
chdir(workDir):
for p in pathList:
inputZip.extract(p)
outputZip.write(p)
def download(url, destination):
"""Asynchronously download url to path specified by destination.
Args:
url: Url location of file.
destination: Path where file should be saved to.
If destination parent directories do not exist, they will be created.
Returns the download thread which can then be joined by the caller to
wait for download completion.
"""
dest_dir = os.path.dirname(destination)
if not os.path.isdir(dest_dir):
os.makedirs(dest_dir)
t = threading.Thread(target=urllib.urlretrieve, args=(url, destination))
t.start()
return t
def update_cipd_package(cipd_yaml_path):
"""Updates the CIPD package specified by cipd_yaml_path.
Args:
cipd_yaml_path: Path of cipd yaml specification file
"""
cipd_yaml_path_abs = os.path.abspath(cipd_yaml_path)
with chdir(os.path.dirname(cipd_yaml_path_abs)),\
tempfile.NamedTemporaryFile() as jsonOut:
ret = cmd_helper.RunCmd([
'cipd', 'create', '-pkg-def', cipd_yaml_path_abs, '-json-output',
jsonOut.name
])
if ret:
raise IOError('Error during cipd create.')
return json.load(jsonOut)['result']['instance_id']
# Copyright 2019 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 os
import re
import tempfile
import shutil
import sys
import unittest
import zipfile
sys.path.append(
os.path.join(
os.path.dirname(__file__), os.pardir, os.pardir, 'third_party',
'pymock'))
from mock import patch # pylint: disable=import-error
sys.path.append(
os.path.join(
os.path.dirname(__file__), os.pardir, os.pardir, 'third_party',
'catapult', 'common', 'py_utils'))
from py_utils import tempfile_ext
import cts_utils
CIPD_DATA = {}
CIPD_DATA['template'] = """# Copyright notice.
# cipd create instructions.
package: %s
description: Dummy Archive
data:
- file: %s
- file: %s
- file: %s
- file: %s
"""
CIPD_DATA['package'] = 'chromium/android_webview/tools/cts_archive'
CIPD_DATA['file1'] = 'arch1/platform1/file1.zip'
CIPD_DATA['file1_arch'] = 'arch1'
CIPD_DATA['file1_platform'] = 'platform1'
CIPD_DATA['file2'] = 'arch1/platform2/file2.zip'
CIPD_DATA['file3'] = 'arch2/platform1/file3.zip'
CIPD_DATA['file4'] = 'arch2/platform2/file4.zip'
CIPD_DATA['yaml'] = CIPD_DATA['template'] % (
CIPD_DATA['package'], CIPD_DATA['file1'], CIPD_DATA['file2'],
CIPD_DATA['file3'], CIPD_DATA['file4'])
CONFIG_DATA = {}
CONFIG_DATA['json'] = """{
"platform1": {
"arch": {
"arch1": {
"filename": "arch1/platform1/file1.zip",
"_origin": "https://a1.p1/f1.zip"
},
"arch2": {
"filename": "arch2/platform1/file3.zip",
"_origin": "https://a2.p1/f3.zip"
}
},
"test_runs": [
{
"apk": "p1/test.apk"
}
]
},
"platform2": {
"arch": {
"arch1": {
"filename": "arch1/platform2/file2.zip",
"_origin": "https://a1.p2/f2.zip"
},
"arch2": {
"filename": "arch2/platform2/file4.zip",
"_origin": "https://a2.p2/f4.zip"
}
},
"test_runs": [
{
"apk": "p2/test1.apk"
},
{
"apk": "p2/test2.apk"
}
]
}
}
"""
CONFIG_DATA['origin11'] = 'https://a1.p1/f1.zip'
CONFIG_DATA['base11'] = 'f1.zip'
CONFIG_DATA['file11'] = 'arch1/platform1/file1.zip'
CONFIG_DATA['origin12'] = 'https://a2.p1/f3.zip'
CONFIG_DATA['base12'] = 'f3.zip'
CONFIG_DATA['file12'] = 'arch2/platform1/file3.zip'
CONFIG_DATA['apk1'] = 'p1/test.apk'
CONFIG_DATA['origin21'] = 'https://a1.p2/f2.zip'
CONFIG_DATA['base21'] = 'f2.zip'
CONFIG_DATA['file21'] = 'arch1/platform2/file2.zip'
CONFIG_DATA['origin22'] = 'https://a2.p2/f4.zip'
CONFIG_DATA['base22'] = 'f4.zip'
CONFIG_DATA['file22'] = 'arch2/platform2/file4.zip'
CONFIG_DATA['apk2a'] = 'p2/test1.apk'
CONFIG_DATA['apk2b'] = 'p2/test2.apk'
DEPS_DATA = {}
DEPS_DATA['template'] = """deps = {
'src/android_webview/tools/cts_archive': {
'packages': [
{
'package': '%s',
'version': '%s',
},
],
'condition': 'checkout_android',
'dep_type': 'cipd',
},
}
"""
DEPS_DATA['revision'] = 'ctsarchiveversion'
DEPS_DATA['deps'] = DEPS_DATA['template'] % (CIPD_DATA['package'],
DEPS_DATA['revision'])
SUITES_DATA = {}
SUITES_DATA['template'] = """{
# Test suites.
'basic_suites': {
'suite1': {
'webview_cts_tests': {
'swarming': {
'shards': 2,
'cipd_packages': [
{
"cipd_package": 'chromium/android_webview/tools/cts_archive',
'location': 'android_webview/tools/cts_archive',
'revision': '%s',
}
]
},
},
},
'suite2': {
'webview_cts_tests': {
'swarming': {
'shards': 2,
'cipd_packages': [
{
"cipd_package": 'chromium/android_webview/tools/cts_archive',
'location': 'android_webview/tools/cts_archive',
'revision': '%s',
}
]
},
},
},
}
}"""
SUITES_DATA['pyl'] = SUITES_DATA['template'] % (DEPS_DATA['revision'],
DEPS_DATA['revision'])
GENERATE_BUILDBOT_JSON = os.path.join('testing', 'buildbot',
'generate_buildbot_json.py')
_CIPD_REFERRERS = [
'DEPS', os.path.join('testing', 'buildbot', 'test_suites.pyl')
]
# Used by check_tempdir.
with tempfile.NamedTemporaryFile() as _f:
_TEMP_DIR = os.path.dirname(_f.name) + os.path.sep
class FakeCIPD(object):
"""Fake CIPD service that supports create and ensure operations."""
_ensure_regex = r'\$ParanoidMode CheckIntegrity[\n\r]+' \
r'@Subdir ([\w/-]+)[\n\r]+' \
r'([\w/-]+) ([\w:]+)[\n\r]*'
_package_json = """{
"result": {
"package": "%s",
"instance_id": "%s"
}
}"""
def __init__(self):
self._yaml = {}
self._fake_version = 0
self._latest_version = {}
def add_package(self, package_def, version):
"""Adds a version, which then becomes available for ensure operations.
Args:
package_def: path to package definition in cipd yaml format. The
contents of each file will be set to the file name string.
version: cipd version
Returns:
json string with same format as that of cipd ensure -json-output
"""
with open(package_def) as def_file:
yaml_dict = cts_utils.CTSCIPDYaml.parse(def_file.readlines())
package = yaml_dict['package']
if package not in self._yaml:
self._yaml[package] = {}
if version in self._yaml[package]:
raise Exception('Attempting to add existing version: ' + version)
self._yaml[package][version] = {}
self._yaml[package][version]['yaml'] = yaml_dict
self._latest_version[package] = version
return self._package_json % (yaml_dict['package'], version)
def get_package(self, package, version):
"""Gets the yaml dict of the package at version
Args:
package: name of cipd package
version: version of cipd package
Returns:
Dictionary of the package in cipd yaml format
"""
return self._yaml[package][version]['yaml']
def get_latest_version(self, package):
return self._latest_version.get(package)
def create(self, package_def, output=None):
"""Implements cipd create -pkg-def <pakcage_def> [-json-output <path>]
Args:
package_def: path to package definition in cipd yaml format. The
contents of each file will be set to the file name string.
output: output file to write json formatted result
Returns:
json string with same format as that of cipd ensure -json-output
"""
version = 'fake_version_' + str(self._fake_version)
json_result = self.add_package(package_def, version)
self._fake_version += 1
if output:
writefile(json_result, output)
return version
def ensure(self, ensure_root, ensure_file):
"""Implements cipd ensure -root <ensure_root> -ensure-file <ensure_file>
Args:
ensure_root: Base directory to copy files to
ensure_file: Path to the cipd ensure file specifying the package version
Raises:
Exception if package and/or version was not previously added
or if ensure file format is not as expected.
"""
ensure_contents = readfile(ensure_file)
match = re.match(self._ensure_regex, ensure_contents)
if match:
subdir = match.group(1)
package = match.group(2)
version = match.group(3)
if package not in self._yaml:
raise Exception('Package not found: ' + package)
if version not in self._yaml[package]:
raise Exception('Version not found: ' + version)
else:
raise Exception('Ensure file not recognized: ' + ensure_contents)
for file_name in [e['file'] for e in \
self._yaml[package][version]['yaml']['data']]:
writefile(file_name,
os.path.join(os.path.abspath(ensure_root), subdir, file_name))
class FakeRunCmd(object):
"""Fake RunCmd that can perform cipd and cp operstions."""
def __init__(self, cipd=None):
self._cipd = cipd
def run_cmd(self, args):
"""Implement devil.utils.cmd_helper.RunCmd.
This doesn't implement cwd kwarg since it's not used by cts_utils
Args:
args: list of args
"""
if (len(args) == 6 and args[:3] == ['cipd', 'ensure', '-root']
and args[4] == '-ensure-file'):
# cipd ensure -root <root> -ensure-file <file>
check_tempdir(os.path.dirname(args[3]))
self._cipd.ensure(args[3], args[5])
elif (len(args) == 6 and args[:3] == ['cipd', 'create', '-pkg-def']
and args[4] == '-json-output'):
# cipd create -pkg-def <def file> -json-output <output file>
check_tempdir(os.path.dirname(args[5]))
self._cipd.create(args[3], args[5])
elif len(args) == 4 and args[:2] == ['cp', '--reflink=never']:
# cp --reflink=never <src> <dest>
check_tempdir(os.path.dirname(args[3]))
shutil.copyfile(args[2], args[3])
elif len(args) == 3 and args[0] == 'cp':
# cp <src> <dest>
check_tempdir(os.path.dirname(args[2]))
shutil.copyfile(args[1], args[2])
else:
raise Exception('Unknown cmd: ' + str(args))
class CTSUtilsTest(unittest.TestCase):
"""Unittests for the cts_utils.py."""
def testCTSCIPDYamlSanity(self):
yaml_data = cts_utils.CTSCIPDYaml(cts_utils.CIPD_PATH)
self.assertTrue(yaml_data.get_package())
self.assertTrue(yaml_data.get_files())
with tempfile.NamedTemporaryFile() as outputFile:
yaml_data.write(outputFile.name)
with open(cts_utils.CIPD_PATH) as cipdFile:
self.assertEqual(cipdFile.readlines(), outputFile.readlines())
def testCTSCIPDYamlOperations(self):
with tempfile.NamedTemporaryFile() as yamlFile:
yamlFile.writelines(CIPD_DATA['yaml'])
yamlFile.flush()
yaml_data = cts_utils.CTSCIPDYaml(yamlFile.name)
self.assertEqual(CIPD_DATA['package'], yaml_data.get_package())
self.assertEqual([
CIPD_DATA['file1'], CIPD_DATA['file2'], CIPD_DATA['file3'],
CIPD_DATA['file4']
], yaml_data.get_files())
yaml_data.append_file('arch2/platform3/file5.zip')
self.assertEqual([
CIPD_DATA['file1'], CIPD_DATA['file2'], CIPD_DATA['file3'],
CIPD_DATA['file4']
] + ['arch2/platform3/file5.zip'], yaml_data.get_files())
yaml_data.remove_file(CIPD_DATA['file1'])
self.assertEqual([
CIPD_DATA['file2'], CIPD_DATA['file3'], CIPD_DATA['file4'],
'arch2/platform3/file5.zip'
], yaml_data.get_files())
with tempfile.NamedTemporaryFile() as yamlFile:
yaml_data.write(yamlFile.name)
new_yaml_contents = readfile(yamlFile.name)
self.assertEqual(
CIPD_DATA['template'] %
(CIPD_DATA['package'], CIPD_DATA['file2'], CIPD_DATA['file3'],
CIPD_DATA['file4'], 'arch2/platform3/file5.zip'), new_yaml_contents)
@patch('devil.utils.cmd_helper.RunCmd')
def testCTSCIPDDownload(self, run_mock):
fake_cipd = FakeCIPD()
fake_run_cmd = FakeRunCmd(cipd=fake_cipd)
run_mock.side_effect = fake_run_cmd.run_cmd
with tempfile.NamedTemporaryFile() as yamlFile,\
tempfile_ext.NamedTemporaryDirectory() as tempDir:
yamlFile.writelines(CIPD_DATA['yaml'])
yamlFile.flush()
fake_version = fake_cipd.create(yamlFile.name)
archive = cts_utils.CTSCIPDYaml(yamlFile.name)
cts_utils.cipd_download(archive, fake_version, tempDir)
self.assertEqual(CIPD_DATA['file1'],
readfile(os.path.join(tempDir, CIPD_DATA['file1'])))
self.assertEqual(CIPD_DATA['file2'],
readfile(os.path.join(tempDir, CIPD_DATA['file2'])))
def testCTSConfigSanity(self):
cts_config = cts_utils.CTSConfig()
platforms = cts_config.get_platforms()
self.assertTrue(platforms)
platform = platforms[0]
archs = cts_config.get_archs(platform)
self.assertTrue(archs)
self.assertTrue(cts_config.get_cipd_zip(platform, archs[0]))
self.assertTrue(cts_config.get_origin(platform, archs[0]))
self.assertTrue(cts_config.get_apks(platform))
def testCTSConfig(self):
with tempfile.NamedTemporaryFile() as configFile:
configFile.writelines(CONFIG_DATA['json'])
configFile.flush()
cts_config = cts_utils.CTSConfig(configFile.name)
self.assertEquals(['platform1', 'platform2'], cts_config.get_platforms())
self.assertEquals(['arch1', 'arch2'], cts_config.get_archs('platform1'))
self.assertEquals(['arch1', 'arch2'], cts_config.get_archs('platform2'))
self.assertEquals('arch1/platform1/file1.zip',
cts_config.get_cipd_zip('platform1', 'arch1'))
self.assertEquals('arch2/platform1/file3.zip',
cts_config.get_cipd_zip('platform1', 'arch2'))
self.assertEquals('arch1/platform2/file2.zip',
cts_config.get_cipd_zip('platform2', 'arch1'))
self.assertEquals('arch2/platform2/file4.zip',
cts_config.get_cipd_zip('platform2', 'arch2'))
self.assertEquals('https://a1.p1/f1.zip',
cts_config.get_origin('platform1', 'arch1'))
self.assertEquals('https://a2.p1/f3.zip',
cts_config.get_origin('platform1', 'arch2'))
self.assertEquals('https://a1.p2/f2.zip',
cts_config.get_origin('platform2', 'arch1'))
self.assertEquals('https://a2.p2/f4.zip',
cts_config.get_origin('platform2', 'arch2'))
self.assertTrue(['p1/test.apk'], cts_config.get_apks('platform1'))
self.assertTrue(['p2/test1.apk', 'p2/test2.apk'],
cts_config.get_apks('platform2'))
def testFilterZip(self):
with tempfile_ext.NamedTemporaryDirectory() as workDir,\
cts_utils.chdir(workDir):
writefile('abc', 'a/b/one.apk')
writefile('def', 'a/b/two.apk')
writefile('ghi', 'a/b/three.apk')
movetozip(['a/b/one.apk', 'a/b/two.apk', 'a/b/three.apk'],
'downloaded.zip')
cts_utils.filterzip('downloaded.zip', ['a/b/one.apk', 'a/b/two.apk'],
'filtered.zip')
zf = zipfile.ZipFile('filtered.zip', 'r')
self.assertEquals(2, len(zf.namelist()))
self.assertEquals('abc', zf.read('a/b/one.apk'))
self.assertEquals('def', zf.read('a/b/two.apk'))
@patch('cts_utils.filterzip')
def testFilterCTS(self, filterzip_mock): # pylint: disable=no-self-use
with tempfile.NamedTemporaryFile() as configFile:
configFile.writelines(CONFIG_DATA['json'])
configFile.flush()
cts_config = cts_utils.CTSConfig(configFile.name)
cts_utils.filter_cts_file(cts_config, CONFIG_DATA['base11'], '/filtered')
filterzip_mock.assert_called_with(
CONFIG_DATA['base11'], [CONFIG_DATA['apk1']],
os.path.join('/filtered', CONFIG_DATA['base11']))
@patch('devil.utils.cmd_helper.RunCmd')
def testUpdateCIPDPackage(self, run_mock):
fake_cipd = FakeCIPD()
fake_run_cmd = FakeRunCmd(cipd=fake_cipd)
run_mock.side_effect = fake_run_cmd.run_cmd
with tempfile_ext.NamedTemporaryDirectory() as tempDir,\
cts_utils.chdir(tempDir):
writefile(CIPD_DATA['yaml'], 'cipd.yaml')
version = cts_utils.update_cipd_package('cipd.yaml')
uploaded = fake_cipd.get_package(CIPD_DATA['package'], version)
self.assertEquals(CIPD_DATA['package'], uploaded['package'])
uploaded_files = [e['file'] for e in uploaded['data']]
self.assertEquals(4, len(uploaded_files))
for i in range(1, 5):
self.assertTrue(CIPD_DATA['file' + str(i)] in uploaded_files)
def testChromiumRepoHelper(self):
with tempfile_ext.NamedTemporaryDirectory() as tempDir,\
cts_utils.chdir(tempDir):
setup_fake_repo('.')
helper = cts_utils.ChromiumRepoHelper(root_dir='.')
self.assertEquals(DEPS_DATA['revision'], helper.get_cipd_dependency_rev())
self.assertEquals(
os.path.join(tempDir, 'a', 'b'), helper.rebase('a', 'b'))
helper.update_cts_cipd_rev('newversion')
self.assertEquals('newversion', helper.get_cipd_dependency_rev())
expected_deps = DEPS_DATA['template'] % (CIPD_DATA['package'],
'newversion')
self.assertEquals(expected_deps, readfile(_CIPD_REFERRERS[0]))
expected_suites = SUITES_DATA['template'] % ('newversion', 'newversion')
self.assertEquals(expected_suites, readfile(_CIPD_REFERRERS[1]))
writefile('#deps not referring to cts cipd', _CIPD_REFERRERS[0])
with self.assertRaises(Exception):
helper.update_cts_cipd_rev('anothernewversion')
@patch('urllib.urlretrieve')
@patch('os.makedirs')
# pylint: disable=no-self-use
def testDownload(self, mock_makedirs, mock_retrieve):
t1 = cts_utils.download('http://www.download.com/file1.zip',
'/download_dir/file1.zip')
t2 = cts_utils.download('http://www.download.com/file2.zip',
'/download_dir/file2.zip')
t1.join()
t2.join()
mock_makedirs.assert_called_with('/download_dir')
mock_retrieve.assert_any_call('http://www.download.com/file1.zip',
'/download_dir/file1.zip')
mock_retrieve.assert_any_call('http://www.download.com/file2.zip',
'/download_dir/file2.zip')
def setup_fake_repo(repoRoot):
"""Populates various files needed for testing cts_utils.
Args:
repo_root: Root of the fake repo under which to write config files
"""
with cts_utils.chdir(repoRoot):
writefile(DEPS_DATA['deps'], cts_utils.DEPS_FILE)
writefile(CONFIG_DATA['json'],
os.path.join(cts_utils.TOOLS_DIR, cts_utils.CONFIG_FILE))
writefile(CIPD_DATA['yaml'],
os.path.join(cts_utils.TOOLS_DIR, cts_utils.CIPD_FILE))
writefile(SUITES_DATA['pyl'], cts_utils.TEST_SUITES_FILE)
def readfile(fpath):
"""Returns contents of file at fpath."""
with open(fpath) as f:
return f.read()
def writefile(contents, path):
"""Writes contents to file at path."""
dir_path = os.path.dirname(os.path.abspath(path))
if not os.path.isdir(dir_path):
os.makedirs(os.path.dirname(path))
with open(path, 'w') as f:
f.write(contents)
def movetozip(fileList, outputPath):
"""Move files in fileList to zip file at outputPath"""
with zipfile.ZipFile(outputPath, 'a') as zf:
for f in fileList:
zf.write(f)
os.remove(f)
def check_tempdir(path):
"""Check if directory at path is under tempdir.
Args:
path: path of directory to check
Raises:
AssertionError if directory is not under tempdir.
"""
abs_path = os.path.abspath(path) + os.path.sep
if abs_path[:len(_TEMP_DIR)] != _TEMP_DIR:
raise AssertionError(
'"%s" is not under tempdir "%s".' % (abs_path, _TEMP_DIR))
if __name__ == '__main__':
unittest.main()
#!/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()
# 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.
import json
import os
import sys
import unittest
import zipfile
sys.path.append(
os.path.join(
os.path.dirname(__file__), os.pardir, os.pardir, 'third_party',
'pymock'))
import mock # pylint: disable=import-error
from mock import call # pylint: disable=import-error
from mock import patch # pylint: disable=import-error
sys.path.append(
os.path.join(
os.path.dirname(__file__), os.pardir, os.pardir, 'third_party',
'catapult', 'common', 'py_utils'))
from py_utils import tempfile_ext
import update_cts
import cts_utils
import cts_utils_test
from cts_utils_test import CIPD_DATA, CONFIG_DATA, DEPS_DATA
from cts_utils_test import GENERATE_BUILDBOT_JSON
def generate_zip_file(path, *files):
"""Create a zip file containing a list of files.
Args:
path: Path to generated zip file
files: one or more file entries in the zip file,
contents of which will be the same as the file's name
"""
path_dir = os.path.dirname(path)
if path_dir and not os.path.isdir(path_dir):
os.makedirs(path_dir)
with zipfile.ZipFile(path, 'w') as zf:
for f in files:
zf.writestr(f, f)
def verify_zip_file(path, *files):
"""Verify zip file that was generated.
Args:
path: Path to generated zip file
files: one or more file entries in the zip file,
contents of which should be the same as the file entry
"""
with zipfile.ZipFile(path) as zf:
names = zf.namelist()
if len(files) != len(names):
raise AssertionError('Expected ' + len(files) + ' files, found ' +
len(names) + '.')
for f in files:
if f not in names:
raise AssertionError(f + ' should be in zip file.')
s = zf.read(f)
if s != f:
raise AssertionError('Expected ' + f + ', found ' + s)
class FakeDownload(object):
"""Allows test to simulate downloads of CTS zip files."""
def __init__(self):
self._files = {}
def add_fake_zip_files(self, cts_config_json):
"""Associate generated zip files to origin urls in config.
Each origin url will be associated with a zip file, contents of which will
be apks specified for that platform. Contents of each apk will just be the
path of that apk as a string.
Args:
cts_config_json: config json string
"""
config = json.loads(cts_config_json)
for p in config:
for a in config[p]['arch']:
o = config[p]['arch'][a]['_origin']
for apk in [e['apk'] for e in config[p]['test_runs']]:
self.append_to_zip_file(o, apk)
def append_to_zip_file(self, url, file_name):
"""Append files to any zip files associated with the url.
If no zip files are associated with the url, one will be created first.
Contents of each file_name will just be the path of that apk as a string.
If file_name is already associated with the url, then this is a no-op.
Args:
url: Url to associate
file_name: Path to add to the zip file associated with the url
"""
if url not in self._files:
self._files[url] = []
if file_name not in self._files[url]:
self._files[url].append(file_name)
def download(self, url, dest):
if url not in self._files:
raise AssertionError('Url should be found: ' + url)
cts_utils_test.check_tempdir(dest)
with zipfile.ZipFile(dest, mode='w') as zf:
for file_name in self._files[url]:
zf.writestr(file_name, file_name)
class UpdateCTSTest(unittest.TestCase):
"""Unittests for update_cts.py."""
@patch('cts_utils.download')
def testDownloadCTS_onePlatform(self, download_mock):
with tempfile_ext.NamedTemporaryDirectory() as workDir,\
tempfile_ext.NamedTemporaryDirectory() as repoRoot:
cts_utils_test.setup_fake_repo(repoRoot)
cts_updater = update_cts.UpdateCTS(workDir, repoRoot)
self.assertEquals(
os.path.join(workDir, 'downloaded'), cts_updater.download_dir)
cts_updater.download_cts(platforms=['platform1'])
download_mock.assert_has_calls([
call(CONFIG_DATA['origin11'],
os.path.join(cts_updater.download_dir, CONFIG_DATA['base11'])),
call(CONFIG_DATA['origin12'],
os.path.join(cts_updater.download_dir, CONFIG_DATA['base12']))
])
self.assertEquals(2, download_mock.call_count)
@patch('cts_utils.download')
def testDownloadCTS_allPlatforms(self, download_mock):
with tempfile_ext.NamedTemporaryDirectory() as workDir,\
tempfile_ext.NamedTemporaryDirectory() as repoRoot:
cts_utils_test.setup_fake_repo(repoRoot)
cts_updater = update_cts.UpdateCTS(workDir, repoRoot)
cts_updater.download_cts()
download_mock.assert_has_calls([
call(CONFIG_DATA['origin11'],
os.path.join(cts_updater.download_dir, CONFIG_DATA['base11'])),
call(CONFIG_DATA['origin12'],
os.path.join(cts_updater.download_dir, CONFIG_DATA['base12'])),
call(CONFIG_DATA['origin21'],
os.path.join(cts_updater.download_dir, CONFIG_DATA['base21'])),
call(CONFIG_DATA['origin22'],
os.path.join(cts_updater.download_dir, CONFIG_DATA['base22']))
])
self.assertEquals(4, download_mock.call_count)
def testFilterCTS(self):
with tempfile_ext.NamedTemporaryDirectory() as workDir,\
tempfile_ext.NamedTemporaryDirectory() as repoRoot,\
cts_utils.chdir(workDir):
cts_utils_test.setup_fake_repo(repoRoot)
cts_updater = update_cts.UpdateCTS('.', repoRoot)
expected_download_dir = os.path.abspath('downloaded')
self.assertEquals(expected_download_dir, cts_updater.download_dir)
os.makedirs(expected_download_dir)
with cts_utils.chdir('downloaded'):
generate_zip_file(CONFIG_DATA['base11'], CONFIG_DATA['apk1'],
'not/a/webview/apk')
generate_zip_file(CONFIG_DATA['base12'], CONFIG_DATA['apk1'],
'not/a/webview/apk')
cts_updater.filter_downloaded_cts()
with cts_utils.chdir('filtered'):
self.assertEquals(2, len(os.listdir('.')))
verify_zip_file(CONFIG_DATA['base11'], CONFIG_DATA['apk1'])
verify_zip_file(CONFIG_DATA['base12'], CONFIG_DATA['apk1'])
@patch('devil.utils.cmd_helper.RunCmd')
def testDownloadCIPD(self, run_mock):
with tempfile_ext.NamedTemporaryDirectory() as workDir,\
tempfile_ext.NamedTemporaryDirectory() as repoRoot,\
cts_utils.chdir(workDir):
cts_utils_test.setup_fake_repo(repoRoot)
fake_cipd = cts_utils_test.FakeCIPD()
fake_cipd.add_package(
os.path.join(repoRoot, cts_utils.TOOLS_DIR, cts_utils.CIPD_FILE),
DEPS_DATA['revision'])
fake_run_cmd = cts_utils_test.FakeRunCmd(fake_cipd)
run_mock.side_effect = fake_run_cmd.run_cmd
cts_updater = update_cts.UpdateCTS('.', repoRoot)
cts_updater.download_cipd()
self.assertTrue(os.path.isdir('cipd'))
for i in [str(x) for x in range(1, 5)]:
self.assertEqual(
CIPD_DATA['file' + i],
cts_utils_test.readfile(
os.path.join(workDir, 'cipd', CIPD_DATA['file' + i])))
def testDownloadCIPD_dirExists(self):
with tempfile_ext.NamedTemporaryDirectory() as workDir,\
tempfile_ext.NamedTemporaryDirectory() as repoRoot,\
cts_utils.chdir(workDir):
cts_utils_test.setup_fake_repo(repoRoot)
cts_updater = update_cts.UpdateCTS('.', repoRoot)
os.makedirs('cipd')
with self.assertRaises(update_cts.DirExistsError):
cts_updater.download_cipd()
def testStageCIPDUpdate(self):
with tempfile_ext.NamedTemporaryDirectory() as workDir,\
tempfile_ext.NamedTemporaryDirectory() as repoRoot,\
cts_utils.chdir(workDir):
cts_utils_test.setup_fake_repo(repoRoot)
cts_utils_test.writefile('n1',
os.path.join('filtered', CONFIG_DATA['base11']))
cts_utils_test.writefile('n3',
os.path.join('filtered', CONFIG_DATA['base12']))
for i in [str(i) for i in range(1, 5)]:
cts_utils_test.writefile('o' + i,
os.path.join('cipd', CIPD_DATA['file' + i]))
cts_updater = update_cts.UpdateCTS('.', repoRoot)
cts_updater.stage_cipd_update()
self.assertTrue(os.path.isdir('staged'))
with cts_utils.chdir('staged'):
self.assertEquals('n1', cts_utils_test.readfile(CONFIG_DATA['file11']))
self.assertEquals('n3', cts_utils_test.readfile(CONFIG_DATA['file12']))
self.assertEquals('o2', cts_utils_test.readfile(CONFIG_DATA['file21']))
self.assertEquals('o4', cts_utils_test.readfile(CONFIG_DATA['file22']))
self.assertEquals(CIPD_DATA['yaml'],
cts_utils_test.readfile('cipd.yaml'))
@patch('cts_utils.update_cipd_package')
def testCommitStagedCIPD(self, update_mock):
with tempfile_ext.NamedTemporaryDirectory() as workDir,\
tempfile_ext.NamedTemporaryDirectory() as repoRoot,\
cts_utils.chdir(workDir):
cts_utils_test.setup_fake_repo(repoRoot)
cts_updater = update_cts.UpdateCTS('.', repoRoot)
with self.assertRaises(update_cts.MissingDirError):
cts_updater.commit_staged_cipd()
cts_utils_test.writefile(CIPD_DATA['yaml'],
os.path.join('staged', 'cipd.yaml'))
with cts_utils.chdir('staged'):
generate_zip_file(CONFIG_DATA['file11'], CONFIG_DATA['apk1'])
generate_zip_file(CONFIG_DATA['file12'], CONFIG_DATA['apk1'])
generate_zip_file(CONFIG_DATA['file21'], CONFIG_DATA['apk2a'],
CONFIG_DATA['apk2b'])
with self.assertRaises(update_cts.MissingFileError):
cts_updater.commit_staged_cipd()
generate_zip_file(CONFIG_DATA['file22'], CONFIG_DATA['apk2a'],
CONFIG_DATA['apk2b'])
update_mock.return_value = 'newcipdversion'
cts_updater.commit_staged_cipd()
update_mock.assert_called_with(
os.path.join(workDir, 'staged', 'cipd.yaml'))
self.assertEquals('newcipdversion',
cts_utils_test.readfile('cipd_version.txt'))
@patch('devil.utils.cmd_helper.RunCmd')
@patch('devil.utils.cmd_helper.GetCmdOutput')
def testUpdateRepository(self, cmd_mock, run_mock):
with tempfile_ext.NamedTemporaryDirectory() as workDir,\
tempfile_ext.NamedTemporaryDirectory() as repoRoot,\
cts_utils.chdir(workDir):
cts_utils_test.setup_fake_repo(repoRoot)
cts_updater = update_cts.UpdateCTS('.', repoRoot)
cts_utils_test.writefile('newversion', 'cipd_version.txt')
cts_utils_test.writefile(CIPD_DATA['yaml'],
os.path.join('staged', 'cipd.yaml'))
cts_utils_test.writefile(
CIPD_DATA['yaml'], os.path.join(os.path.join('staged', 'cipd.yaml')))
cmd_mock.return_value = ''
run_mock.return_value = 0
cts_updater.update_repository()
self._assertCIPDVersionUpdated(repoRoot, 'newversion')
repo_cipd_yaml = os.path.join(repoRoot, cts_utils.TOOLS_DIR,
cts_utils.CIPD_FILE)
run_mock.assert_any_call(
['cp',
os.path.join(workDir, 'staged', 'cipd.yaml'), repo_cipd_yaml])
run_mock.assert_any_call([
'cipd', 'ensure', '-root',
os.path.dirname(repo_cipd_yaml), '-ensure-file', mock.ANY
])
run_mock.assert_any_call(['python', GENERATE_BUILDBOT_JSON])
@patch('devil.utils.cmd_helper.RunCmd')
@patch('devil.utils.cmd_helper.GetCmdOutput')
def testUpdateRepository_uncommitedChanges(self, cmd_mock, run_mock):
with tempfile_ext.NamedTemporaryDirectory() as workDir,\
tempfile_ext.NamedTemporaryDirectory() as repoRoot,\
cts_utils.chdir(workDir):
cts_utils_test.setup_fake_repo(repoRoot)
cts_updater = update_cts.UpdateCTS('.', repoRoot)
cts_utils_test.writefile('newversion', 'cipd_version.txt')
cts_utils_test.writefile(CIPD_DATA['yaml'],
os.path.join('staged', 'cipd.yaml'))
cmd_mock.return_value = 'M DEPS'
run_mock.return_value = 0
with self.assertRaises(update_cts.UncommittedChangeException):
cts_updater.update_repository()
@patch('devil.utils.cmd_helper.RunCmd')
@patch('devil.utils.cmd_helper.GetCmdOutput')
def testUpdateRepository_buildbotUpdateError(self, cmd_mock, run_mock):
with tempfile_ext.NamedTemporaryDirectory() as workDir,\
tempfile_ext.NamedTemporaryDirectory() as repoRoot,\
cts_utils.chdir(workDir):
cts_utils_test.setup_fake_repo(repoRoot)
cts_updater = update_cts.UpdateCTS('.', repoRoot)
cts_utils_test.writefile('newversion', 'cipd_version.txt')
cts_utils_test.writefile(CIPD_DATA['yaml'],
os.path.join('staged', 'cipd.yaml'))
cmd_mock.return_value = ''
run_mock.return_value = 1
with self.assertRaises(IOError):
cts_updater.update_repository()
@patch('devil.utils.cmd_helper.RunCmd')
@patch('devil.utils.cmd_helper.GetCmdOutput')
def testUpdateRepository_inconsistentFiles(self, cmd_mock, run_mock):
with tempfile_ext.NamedTemporaryDirectory() as workDir,\
tempfile_ext.NamedTemporaryDirectory() as repoRoot,\
cts_utils.chdir(workDir):
cts_utils_test.setup_fake_repo(repoRoot)
cts_updater = update_cts.UpdateCTS('.', repoRoot)
cts_utils_test.writefile('newversion', 'cipd_version.txt')
cts_utils_test.writefile(CIPD_DATA['yaml'],
os.path.join('staged', 'cipd.yaml'))
cmd_mock.return_value = ''
run_mock.return_value = 0
cts_utils_test.writefile(
CIPD_DATA['template'] %
('wrong/package/name', CIPD_DATA['file1'], CIPD_DATA['file2'],
CIPD_DATA['file3'], CIPD_DATA['file4']),
os.path.join(os.path.join('staged', 'cipd.yaml')))
with self.assertRaises(update_cts.InconsistentFilesException):
cts_updater.update_repository()
@patch('devil.utils.cmd_helper.RunCmd')
@patch('devil.utils.cmd_helper.GetCmdOutput')
@patch.object(cts_utils.ChromiumRepoHelper, 'update_testing_json')
@patch('urllib.urlretrieve')
def testCompleteUpdate(self, retrieve_mock, update_json_mock, cmd_mock,
run_mock):
with tempfile_ext.NamedTemporaryDirectory() as workDir,\
tempfile_ext.NamedTemporaryDirectory() as repoRoot,\
cts_utils.chdir(workDir):
cts_utils_test.setup_fake_repo(repoRoot)
fake_cipd = cts_utils_test.FakeCIPD()
fake_cipd.add_package(
os.path.join(repoRoot, cts_utils.TOOLS_DIR, cts_utils.CIPD_FILE),
DEPS_DATA['revision'])
fake_download = FakeDownload()
fake_download.add_fake_zip_files(CONFIG_DATA['json'])
fake_run_cmd = cts_utils_test.FakeRunCmd(fake_cipd)
retrieve_mock.side_effect = fake_download.download
run_mock.side_effect = fake_run_cmd.run_cmd
update_json_mock.return_value = 0
cmd_mock.return_value = ''
cts_updater = update_cts.UpdateCTS('.', repoRoot)
cts_updater.download_cts_cmd()
cts_updater.create_cipd_cmd()
cts_updater.update_repository_cmd()
latest_version = fake_cipd.get_latest_version(
'chromium/android_webview/tools/cts_archive')
self.assertNotEquals(DEPS_DATA['revision'], latest_version)
self._assertCIPDVersionUpdated(repoRoot, latest_version)
repo_cipd_yaml = os.path.join(repoRoot, cts_utils.TOOLS_DIR,
cts_utils.CIPD_FILE)
run_mock.assert_any_call(
['cp',
os.path.join(workDir, 'staged', 'cipd.yaml'), repo_cipd_yaml])
run_mock.assert_any_call([
'cipd', 'ensure', '-root',
os.path.dirname(repo_cipd_yaml), '-ensure-file', mock.ANY
])
update_json_mock.assert_called_with()
def _assertCIPDVersionUpdated(self, repo_root, new_version):
"""Check that cts cipd version in DEPS and test suites were updated.
Args:
repo_root: Root directory of checkout
new_version: Expected version of CTS package
Raises:
AssertionError: If contents of DEPS and test suite files were not
expected.
"""
self.assertEquals(
DEPS_DATA['template'] % (CIPD_DATA['package'], new_version),
cts_utils_test.readfile(os.path.join(repo_root, 'DEPS')))
self.assertEquals(
cts_utils_test.SUITES_DATA['template'] % (new_version, new_version),
cts_utils_test.readfile(
os.path.join(repo_root, 'testing', 'buildbot', 'test_suites.pyl')))
if __name__ == '__main__':
unittest.main()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment