You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Tools/LauncherTestTools/device_farm_schedule_run.py

292 lines
13 KiB
Python

"""
All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
its licensors.
For complete copyright and license terms please see the LICENSE at the root of this
distribution (the "License"). All use of this software is governed by the License,
or, if provided, by the license below or the license accompanying this file. Do not
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
Device Farm Schecule Run
"""
import argparse
import datetime
import json
import logging
import os
import subprocess
import sys
import time
import requests
logger = logging.getLogger(__name__)
def bake_template(filename, values):
"""Open a template and replace values. Return path to baked file."""
# Open the options json template and replace with real values.
with open(filename, 'r') as in_file:
data = in_file.read()
for key, value in values.iteritems():
data = data.replace(key, str(value))
filename_out = os.path.join('temp', filename)
with open(filename_out, 'w') as out_file:
out_file.write(data)
return filename_out
def execute_aws_command(args):
""" Execut the aws cli devicefarm command. """
# Use .cmd on Windows, not sure exactly why, but aws will not be found without it.
aws_executable = 'aws.cmd' if sys.platform.startswith('win') else 'aws'
aws_args = [aws_executable, 'devicefarm', '--region', 'us-west-2'] + args
logger.info("Running {} ...".format(" ".join(aws_args)))
p = subprocess.Popen(aws_args, stdout=subprocess.PIPE)
out, err = p.communicate()
if p.returncode != 0:
msg = "Command '{}' failed. return code: {} out: {} err: {}".format(
" ".join(aws_args),
p.returncode,
out,
err
)
raise Exception(msg)
return out
def find_or_create_project(project_name):
""" Find the project by name, or create a new one. """
list_projects_data = json.loads(execute_aws_command(['list-projects']))
# return the arn if it is found
for project_data in list_projects_data['projects']:
if project_data['name'] == project_name:
logger.info("Found existing project named {}.".format(project_name))
return project_data['arn']
# project not found, create a new project with the give name
project_data = json.loads(execute_aws_command(['create-project', '--name', project_name]))
return project_data['project']['arn']
def find_or_create_device_pool(project_name, device_pool_name, device_arns):
""" Find the device pool in the project by name, or create a new one. """
list_device_pools_data = json.loads(execute_aws_command(['list-device-pools', '--arn', project_name]))
# return the arn if it is found
for device_pool_data in list_device_pools_data['devicePools']:
if device_pool_data['name'] == device_pool_name:
logger.info("Found existing device pool named {}.".format(device_pool_name))
return device_pool_data['arn']
device_pool_json_path_out = bake_template(
'device_farm_default_device_pool_template.json',
{'%DEVICE_ARN_LIST%' : device_arns})
# create a default device pool
args = [
'create-device-pool',
'--project-arn',
project_name,
'--name',
device_pool_name,
'--rules',
"file://{}".format(device_pool_json_path_out)]
device_pools_data = json.loads(execute_aws_command(args))
return device_pools_data['devicePool']['arn']
def create_upload(project_arn, path, type):
""" Create an upload and return the ARN """
args = ['create-upload', '--project-arn', project_arn, '--name', os.path.basename(path), '--type', type]
upload_data = json.loads(execute_aws_command(args))
return upload_data['upload']['arn'], upload_data['upload']['url']
def send_upload(filename, url):
""" Upload a file with a put request. """
logger.info("Sending upload {} ...".format(filename))
with open(filename, 'rb') as uploadfile:
data = uploadfile.read()
headers = {"content-type": "application/octet-stream"}
output = requests.put(url, data=data, allow_redirects=True, headers=headers)
logger.info("Sent upload {}.".format(output))
def wait_for_upload_to_finish(poll_time, upload_arn):
""" Wait for an upload to finish by polling for status """
logger.info("Waiting for upload {} ...".format(upload_arn))
upload_data = json.loads(execute_aws_command(['get-upload', '--arn', upload_arn]))
while not upload_data['upload']['status'] in ['SUCCEEDED', 'FAILED']:
time.sleep(poll_time)
upload_data = json.loads(execute_aws_command(['get-upload', '--arn', upload_arn]))
if upload_data['upload']['status'] != 'SUCCEEDED':
raise Exception('Upload failed.')
def upload(poll_time, project_arn, path, type):
""" Create the upload on the Device Farm, upload the file and wait for completion. """
arn, url = create_upload(project_arn, path, type)
send_upload(path, url)
wait_for_upload_to_finish(poll_time, arn)
return arn
def schedule_run(project_arn, app_arn, device_pool_arn, test_spec_arn, test_bundle_arn, execution_timeout):
""" Schecule the test run on the Device Farm """
run_name = "LY LT {}".format(datetime.datetime.now().strftime("%I:%M%p on %B %d, %Y"))
logger.info("Scheduling run {} ...".format(run_name))
schedule_run_test_json_path_out = bake_template(
'device_farm_schedule_run_test_template.json',
{'%TEST_SPEC_ARN%' : test_spec_arn, '%TEST_PACKAGE_ARN%' : test_bundle_arn})
execution_configuration_json_path_out = bake_template(
'device_farm_schedule_run_execution_configuration_template.json',
{'%EXECUTION_TIMEOUT%' : execution_timeout})
args = [
'schedule-run',
'--project-arn',
project_arn,
'--app-arn',
app_arn,
'--device-pool-arn',
device_pool_arn,
'--name',
"\"{}\"".format(run_name),
'--test',
"file://{}".format(schedule_run_test_json_path_out),
'--execution-configuration',
"file://{}".format(execution_configuration_json_path_out)]
schedule_run_data = json.loads(execute_aws_command(args))
return schedule_run_data['run']['arn']
def download_file(url, output_path):
""" download a file from a url, save in output_path """
try:
r = requests.get(url, stream=True)
r.raise_for_status()
output_folder = os.path.dirname(output_path)
if not os.path.exists(output_folder):
os.makedirs(output_folder)
with open(output_path, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
except requests.exceptions.RequestException as e:
logging.exception("Failed request for downloading file from {}.".format(url))
return False
except IOError as e:
logging.exception("Failed writing to file {}.".format(output_path))
return False
return True
def download_artifacts(run_arn, artifacts_output_folder):
"""
Download run artifacts and write to path set in artifacts_output_folder.
"""
logging.basicConfig(level=logging.DEBUG)
list_jobs_data = json.loads(execute_aws_command(['list-jobs', '--arn', run_arn]))
for job_data in list_jobs_data['jobs']:
logger.info("Downloading artifacts for {} ...".format(job_data['name']))
safe_job_name = "".join(x for x in job_data['name'] if x.isalnum())
list_artifacts_data = json.loads(execute_aws_command(['list-artifacts', '--arn', job_data['arn'], '--type', 'FILE']))
for artifact_data in list_artifacts_data['artifacts']:
# A run may contain many jobs. Usually each job is one device type.
# Each job has 3 stages: setup, test and shutdown. You can tell what
# stage an artifact is from based on the ARN.
# We only care about artifacts from the main stage of the job,
# not the setup or tear down artifacts. So parse the ARN and look
# for the 00001 identifier.
print artifact_data['arn']
if artifact_data['arn'].split('/')[3] == '00001':
logger.info("Downloading artifacts {} ...".format(artifact_data['name']))
output_filename = "{}.{}".format(
"".join(x for x in artifact_data['name'] if x.isalnum()),
artifact_data['extension'])
output_path = os.path.join(artifacts_output_folder, safe_job_name, output_filename)
if not download_file(artifact_data['url'], output_path):
msg = "Failed to download file from {} and save to {}".format(artifact_data['url'], output_path)
logger.error(msg)
def main():
parser = argparse.ArgumentParser(description='Upload and app and schedule a run on the Device Farm.')
parser.add_argument('--app-path', required=True, help='Path of the app file.')
parser.add_argument('--test-spec-path', required=True, help='Path of the test spec yaml.')
parser.add_argument('--test-bundle-path', required=True, help='Path of the test bundle zip.')
parser.add_argument('--project-name', required=True, help='The name of the project.')
parser.add_argument('--device-pool-name', required=True, help='The name of the device pool.')
parser.add_argument('--device-arns',
default='\\"arn:aws:devicefarm:us-west-2::device:6CCDF49186B64E3FB27B9346AC9FAEC1\\"',
help='List of device ARNs. Used when existing pool is not found by name. Default is Galaxy S8.')
parser.add_argument('--wait-for-result', default="true", help='Set to "true" to wait for result of run.')
parser.add_argument('--download-artifacts', default="true", help='Set to "true" to download artifacts after run. requires --wait-for-result')
parser.add_argument('--artifacts-output-folder', default="temp", help='Folder to place the downloaded artifacts.')
parser.add_argument('--upload-poll-time', default=10, help='How long to wait between polling upload status.')
parser.add_argument('--run-poll-time', default=60, help='How long to wait between polling run status.')
parser.add_argument('--run-execution-timeout', default=60, help='Run execution timeout.')
parser.add_argument('--test-names', nargs='+', help='A list of test names to run, default runs all tests.')
args = parser.parse_args()
logging.basicConfig(level=logging.DEBUG)
# Find the project by name, or create a new one.
project_arn = find_or_create_project(args.project_name)
# Find the device pool in the project by name, or create a new one.
device_pool_arn = find_or_create_device_pool(project_arn, args.device_pool_name, args.device_arns)
# Bake out EXTRA_ARGS option with args.test_names
extra_args = ""
if args.test_names:
extra_args = "--test-names {}".format(" ".join("\"{}\"".format(test_name) for test_name in args.test_names))
test_spec_path_out = bake_template(
args.test_spec_path,
{'%EXTRA_ARGS%' : extra_args})
# Upload test spec and test bundle (Appium js is just a generic avenue to our own custom code).
test_spec_arn = upload(args.upload_poll_time, project_arn, test_spec_path_out, 'APPIUM_NODE_TEST_SPEC')
test_bundle_arn = upload(args.upload_poll_time, project_arn, args.test_bundle_path, 'APPIUM_NODE_TEST_PACKAGE')
# Upload the app.
type = 'ANDROID_APP' if args.app_path.lower().endswith('.apk') else 'IOS_APP'
app_arn = upload(args.upload_poll_time, project_arn, args.app_path, type)
# Schedule the test run.
run_arn = schedule_run(project_arn, app_arn, device_pool_arn, test_spec_arn, test_bundle_arn, args.run_execution_timeout)
logger.info('Run scheduled.')
# Wait for run, exit with failure if test run fails.
# strcmp with true for easy of use jenkins boolean env var.
if args.wait_for_result.lower() == 'true':
# Runs can take a long time, so just poll once a mintue by default.
run_data = json.loads(execute_aws_command(['get-run', '--arn', run_arn]))
while run_data['run']['result'] == 'PENDING':
logger.info("Run status: {} waiting {} seconds ...".format(run_data['run']['result'], args.run_poll_time))
time.sleep(args.run_poll_time)
run_data = json.loads(execute_aws_command(['get-run', '--arn', run_arn]))
# Download run artifacts. strcmp with true for easy of use jenkins boolean env var.
if args.download_artifacts.lower() == 'true':
download_artifacts(run_arn, args.artifacts_output_folder)
# If the run did not pass raise an exception to fail this jenkins job.
if run_data['run']['result'] != 'PASSED':
# Dump all of the run info.
logger.info(run_data)
# Raise an exception to fail this test.
msg = "Run fail with result {}\nRun ARN: {}".format(run_data['run']['result'], run_arn)
raise Exception(msg)
logger.info('Run passed.')
if __name__== "__main__":
main()