# -*- coding: utf-8 -*- # # Copyright 2017 Google LLC. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Create Android test matrices in Firebase Test Lab.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import os import uuid from apitools.base.py import exceptions as apitools_exceptions from googlecloudsdk.api_lib.firebase.test import matrix_creator_common from googlecloudsdk.api_lib.firebase.test import matrix_ops from googlecloudsdk.api_lib.firebase.test import util from googlecloudsdk.calliope import exceptions from googlecloudsdk.core import log import six def CreateMatrix(args, context, history_id, gcs_results_root, release_track): """Creates a new matrix test in Firebase Test Lab from the user's params. Args: args: an argparse namespace. All the arguments that were provided to this gcloud command invocation (i.e. group and command arguments combined). context: {str:obj} dict containing the gcloud command context, which includes the Testing API client+messages libs generated by Apitools. history_id: {str} A history ID to publish Tool Results to. gcs_results_root: the root dir for a matrix within the GCS results bucket. release_track: the release track that the command is invoked from. Returns: A TestMatrix object created from the supplied matrix configuration values. """ creator = MatrixCreator(args, context, history_id, gcs_results_root, release_track) return creator.CreateTestMatrix(uuid.uuid4().hex) class MatrixCreator(object): """Creates a single test matrix based on user-supplied test arguments.""" def __init__(self, args, context, history_id, gcs_results_root, release_track): """Construct a MatrixCreator to be used to create a single test matrix. Args: args: an argparse namespace. All the arguments that were provided to this gcloud command invocation (i.e. group and command arguments combined). context: {str:obj} dict containing the gcloud command context, which includes the Testing API client+messages libs generated by Apitools. history_id: {str} A history ID to publish Tool Results to. gcs_results_root: the root dir for a matrix within the GCS results bucket. release_track: the release track that the command is invoked from. """ self._project = util.GetProject() self._args = args self._history_id = history_id self._gcs_results_root = gcs_results_root self._client = context['testing_client'] self._messages = context['testing_messages'] self._release_track = release_track def _BuildAppReference(self, filename): """Builds either a FileReference or an AppBundle message for a file.""" if filename.endswith('.aab'): return None, self._messages.AppBundle( bundleLocation=self._BuildFileReference(os.path.basename(filename))) else: return self._BuildFileReference(os.path.basename(filename)), None def _BuildFileReference(self, filename): """Build a FileReference pointing to the GCS copy of a file.""" return self._messages.FileReference( gcsPath=os.path.join(self._gcs_results_root, filename)) def _GetOrchestratorOption(self): orchestrator_options = ( self._messages.AndroidInstrumentationTest .OrchestratorOptionValueValuesEnum) if self._args.use_orchestrator is None: return orchestrator_options.ORCHESTRATOR_OPTION_UNSPECIFIED elif self._args.use_orchestrator: return orchestrator_options.USE_ORCHESTRATOR else: return orchestrator_options.DO_NOT_USE_ORCHESTRATOR def _BuildRoboDirectives(self, robo_directives_dict): """Build a list of RoboDirectives from the dictionary input.""" robo_directives = [] action_types = self._messages.RoboDirective.ActionTypeValueValuesEnum action_type_mapping = { 'click': action_types.SINGLE_CLICK, 'text': action_types.ENTER_TEXT, 'ignore': action_types.IGNORE } for key, value in six.iteritems((robo_directives_dict or {})): (action_type, resource_name) = util.ParseRoboDirectiveKey(key) robo_directives.append( self._messages.RoboDirective( resourceName=resource_name, inputText=value, actionType=action_type_mapping.get(action_type))) return robo_directives def _BuildAndroidInstrumentationTestSpec(self): """Build a TestSpecification for an AndroidInstrumentationTest.""" spec = self._BuildGenericTestSpec() app_apk, app_bundle = self._BuildAppReference(self._args.app) spec.androidInstrumentationTest = self._messages.AndroidInstrumentationTest( appApk=app_apk, appBundle=app_bundle, testApk=self._BuildFileReference(os.path.basename(self._args.test)), appPackageId=self._args.app_package, testPackageId=self._args.test_package, testRunnerClass=self._args.test_runner_class, testTargets=(self._args.test_targets or []), orchestratorOption=self._GetOrchestratorOption(), shardingOption=self._BuildShardingOption()) return spec def _BuildAndroidRoboTestSpec(self): """Build a TestSpecification for an AndroidRoboTest.""" spec = self._BuildGenericTestSpec() app_apk, app_bundle = self._BuildAppReference(self._args.app) spec.androidRoboTest = self._messages.AndroidRoboTest( appApk=app_apk, appBundle=app_bundle, appPackageId=self._args.app_package, roboDirectives=self._BuildRoboDirectives(self._args.robo_directives)) if getattr(self._args, 'robo_script', None): spec.androidRoboTest.roboScript = self._BuildFileReference( os.path.basename(self._args.robo_script)) return spec def _BuildAndroidGameLoopTestSpec(self): """Build a TestSpecification for an AndroidTestLoop.""" spec = self._BuildGenericTestSpec() app_apk, app_bundle = self._BuildAppReference(self._args.app) spec.androidTestLoop = self._messages.AndroidTestLoop( appApk=app_apk, appBundle=app_bundle, appPackageId=self._args.app_package) if self._args.scenario_numbers: spec.androidTestLoop.scenarios = self._args.scenario_numbers if self._args.scenario_labels: spec.androidTestLoop.scenarioLabels = self._args.scenario_labels return spec def _BuildGenericTestSpec(self): """Build a generic TestSpecification without test-type specifics.""" device_files = [] for obb_file in self._args.obb_files or []: obb_file_name = os.path.basename(obb_file) device_files.append( self._messages.DeviceFile( obbFile=self._messages.ObbFile( obbFileName=obb_file_name, obb=self._BuildFileReference(obb_file_name)))) other_files = getattr(self._args, 'other_files', None) or {} for device_path in other_files.keys(): device_files.append( self._messages.DeviceFile( regularFile=self._messages.RegularFile( content=self._BuildFileReference( util.GetRelativeDevicePath(device_path)), devicePath=device_path))) environment_variables = [] if self._args.environment_variables: for key, value in six.iteritems(self._args.environment_variables): environment_variables.append( self._messages.EnvironmentVariable(key=key, value=value)) directories_to_pull = self._args.directories_to_pull or [] account = None if self._args.auto_google_login: account = self._messages.Account(googleAuto=self._messages.GoogleAuto()) additional_apks = [ self._messages.Apk( location=self._BuildFileReference(os.path.basename(additional_apk))) for additional_apk in getattr(self._args, 'additional_apks', []) or [] ] grant_permissions = getattr(self._args, 'grant_permissions', 'all') == 'all' setup = self._messages.TestSetup( filesToPush=device_files, account=account, environmentVariables=environment_variables, directoriesToPull=directories_to_pull, networkProfile=getattr(self._args, 'network_profile', None), additionalApks=additional_apks, dontAutograntPermissions=not grant_permissions) return self._messages.TestSpecification( testTimeout=matrix_ops.ReformatDuration(self._args.timeout), testSetup=setup, disableVideoRecording=not self._args.record_video, disablePerformanceMetrics=not self._args.performance_metrics) def _BuildShardingOption(self): """Build a ShardingOption for an AndroidInstrumentationTest.""" if getattr(self._args, 'num_uniform_shards', {}): return self._messages.ShardingOption( uniformSharding=self._messages.UniformSharding( numShards=self._args.num_uniform_shards)) elif getattr(self._args, 'test_targets_for_shard', {}): return self._messages.ShardingOption( manualSharding=self._BuildManualShard( self._args.test_targets_for_shard)) def _BuildManualShard(self, test_targets_for_shard): """Build a ManualShard for a ShardingOption.""" test_targets = [ self._BuildTestTargetsForShard(test_target) for test_target in test_targets_for_shard ] return self._messages.ManualSharding(testTargetsForShard=test_targets) def _BuildTestTargetsForShard(self, test_targets_for_each_shard): return self._messages.TestTargetsForShard(testTargets=[ target for target in test_targets_for_each_shard.split(';') if target is not None ]) def _TestSpecFromType(self, test_type): """Map a test type into its corresponding TestSpecification message.""" if test_type == 'instrumentation': return self._BuildAndroidInstrumentationTestSpec() elif test_type == 'robo': return self._BuildAndroidRoboTestSpec() elif test_type == 'game-loop': return self._BuildAndroidGameLoopTestSpec() else: # It's a bug in our arg validation if we ever get here. raise exceptions.InvalidArgumentException( 'type', 'Unknown test type "{}".'.format(test_type)) def _BuildTestMatrix(self, spec): """Build just the user-specified parts of a TestMatrix message. Args: spec: a TestSpecification message corresponding to the test type. Returns: A TestMatrix message. """ if self._args.device: devices = [self._BuildAndroidDevice(d) for d in self._args.device] environment_matrix = self._messages.EnvironmentMatrix( androidDeviceList=self._messages.AndroidDeviceList( androidDevices=devices)) else: environment_matrix = self._messages.EnvironmentMatrix( androidMatrix=self._messages.AndroidMatrix( androidModelIds=self._args.device_ids, androidVersionIds=self._args.os_version_ids, locales=self._args.locales, orientations=self._args.orientations)) gcs = self._messages.GoogleCloudStorage(gcsPath=self._gcs_results_root) hist = self._messages.ToolResultsHistory(projectId=self._project, historyId=self._history_id) results = self._messages.ResultStorage(googleCloudStorage=gcs, toolResultsHistory=hist) client_info = matrix_creator_common.BuildClientInfo( self._messages, getattr(self._args, 'client_details', {}) or {}, self._release_track) return self._messages.TestMatrix( testSpecification=spec, environmentMatrix=environment_matrix, clientInfo=client_info, resultStorage=results, flakyTestAttempts=self._args.num_flaky_test_attempts or 0) def _BuildAndroidDevice(self, device_map): return self._messages.AndroidDevice( androidModelId=device_map['model'], androidVersionId=device_map['version'], locale=device_map['locale'], orientation=device_map['orientation']) def _BuildTestMatrixRequest(self, request_id): """Build a TestingProjectsTestMatricesCreateRequest for a test matrix. Args: request_id: {str} a unique ID for the CreateTestMatrixRequest. Returns: A TestingProjectsTestMatricesCreateRequest message. """ spec = self._TestSpecFromType(self._args.type) return self._messages.TestingProjectsTestMatricesCreateRequest( projectId=self._project, testMatrix=self._BuildTestMatrix(spec), requestId=request_id) def CreateTestMatrix(self, request_id): """Invoke the Testing service to create a test matrix from the user's args. Args: request_id: {str} a unique ID for the CreateTestMatrixRequest. Returns: The TestMatrix response message from the TestMatrices.Create rpc. Raises: HttpException if the test service reports an HttpError. """ request = self._BuildTestMatrixRequest(request_id) log.debug('TestMatrices.Create request:\n{0}\n'.format(request)) try: response = self._client.projects_testMatrices.Create(request) log.debug('TestMatrices.Create response:\n{0}\n'.format(response)) except apitools_exceptions.HttpError as error: msg = 'Http error while creating test matrix: ' + util.GetError(error) raise exceptions.HttpException(msg) log.status.Print('Test [{id}] has been created in the Google Cloud.' .format(id=response.testMatrixId)) return response