1# -*- coding: utf-8 -*- # 2# Copyright 2017 Google LLC. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16"""Create Android test matrices in Firebase Test Lab.""" 17 18from __future__ import absolute_import 19from __future__ import division 20from __future__ import unicode_literals 21 22import os 23import uuid 24 25from apitools.base.py import exceptions as apitools_exceptions 26 27from googlecloudsdk.api_lib.firebase.test import matrix_creator_common 28from googlecloudsdk.api_lib.firebase.test import matrix_ops 29from googlecloudsdk.api_lib.firebase.test import util 30from googlecloudsdk.calliope import exceptions 31from googlecloudsdk.core import log 32import six 33 34 35def CreateMatrix(args, context, history_id, gcs_results_root, release_track): 36 """Creates a new matrix test in Firebase Test Lab from the user's params. 37 38 Args: 39 args: an argparse namespace. All the arguments that were provided to this 40 gcloud command invocation (i.e. group and command arguments combined). 41 context: {str:obj} dict containing the gcloud command context, which 42 includes the Testing API client+messages libs generated by Apitools. 43 history_id: {str} A history ID to publish Tool Results to. 44 gcs_results_root: the root dir for a matrix within the GCS results bucket. 45 release_track: the release track that the command is invoked from. 46 47 Returns: 48 A TestMatrix object created from the supplied matrix configuration values. 49 """ 50 creator = MatrixCreator(args, context, history_id, gcs_results_root, 51 release_track) 52 return creator.CreateTestMatrix(uuid.uuid4().hex) 53 54 55class MatrixCreator(object): 56 """Creates a single test matrix based on user-supplied test arguments.""" 57 58 def __init__(self, args, context, history_id, gcs_results_root, 59 release_track): 60 """Construct a MatrixCreator to be used to create a single test matrix. 61 62 Args: 63 args: an argparse namespace. All the arguments that were provided to this 64 gcloud command invocation (i.e. group and command arguments combined). 65 context: {str:obj} dict containing the gcloud command context, which 66 includes the Testing API client+messages libs generated by Apitools. 67 history_id: {str} A history ID to publish Tool Results to. 68 gcs_results_root: the root dir for a matrix within the GCS results bucket. 69 release_track: the release track that the command is invoked from. 70 """ 71 self._project = util.GetProject() 72 self._args = args 73 self._history_id = history_id 74 self._gcs_results_root = gcs_results_root 75 self._client = context['testing_client'] 76 self._messages = context['testing_messages'] 77 self._release_track = release_track 78 79 def _BuildAppReference(self, filename): 80 """Builds either a FileReference or an AppBundle message for a file.""" 81 if filename.endswith('.aab'): 82 return None, self._messages.AppBundle( 83 bundleLocation=self._BuildFileReference(os.path.basename(filename))) 84 else: 85 return self._BuildFileReference(os.path.basename(filename)), None 86 87 def _BuildFileReference(self, filename): 88 """Build a FileReference pointing to the GCS copy of a file.""" 89 return self._messages.FileReference( 90 gcsPath=os.path.join(self._gcs_results_root, filename)) 91 92 def _GetOrchestratorOption(self): 93 orchestrator_options = ( 94 self._messages.AndroidInstrumentationTest 95 .OrchestratorOptionValueValuesEnum) 96 if self._args.use_orchestrator is None: 97 return orchestrator_options.ORCHESTRATOR_OPTION_UNSPECIFIED 98 elif self._args.use_orchestrator: 99 return orchestrator_options.USE_ORCHESTRATOR 100 else: 101 return orchestrator_options.DO_NOT_USE_ORCHESTRATOR 102 103 def _BuildRoboDirectives(self, robo_directives_dict): 104 """Build a list of RoboDirectives from the dictionary input.""" 105 robo_directives = [] 106 action_types = self._messages.RoboDirective.ActionTypeValueValuesEnum 107 action_type_mapping = { 108 'click': action_types.SINGLE_CLICK, 109 'text': action_types.ENTER_TEXT, 110 'ignore': action_types.IGNORE 111 } 112 for key, value in six.iteritems((robo_directives_dict or {})): 113 (action_type, resource_name) = util.ParseRoboDirectiveKey(key) 114 robo_directives.append( 115 self._messages.RoboDirective( 116 resourceName=resource_name, 117 inputText=value, 118 actionType=action_type_mapping.get(action_type))) 119 return robo_directives 120 121 def _BuildAndroidInstrumentationTestSpec(self): 122 """Build a TestSpecification for an AndroidInstrumentationTest.""" 123 spec = self._BuildGenericTestSpec() 124 app_apk, app_bundle = self._BuildAppReference(self._args.app) 125 spec.androidInstrumentationTest = self._messages.AndroidInstrumentationTest( 126 appApk=app_apk, 127 appBundle=app_bundle, 128 testApk=self._BuildFileReference(os.path.basename(self._args.test)), 129 appPackageId=self._args.app_package, 130 testPackageId=self._args.test_package, 131 testRunnerClass=self._args.test_runner_class, 132 testTargets=(self._args.test_targets or []), 133 orchestratorOption=self._GetOrchestratorOption(), 134 shardingOption=self._BuildShardingOption()) 135 return spec 136 137 def _BuildAndroidRoboTestSpec(self): 138 """Build a TestSpecification for an AndroidRoboTest.""" 139 spec = self._BuildGenericTestSpec() 140 app_apk, app_bundle = self._BuildAppReference(self._args.app) 141 spec.androidRoboTest = self._messages.AndroidRoboTest( 142 appApk=app_apk, 143 appBundle=app_bundle, 144 appPackageId=self._args.app_package, 145 roboDirectives=self._BuildRoboDirectives(self._args.robo_directives)) 146 if getattr(self._args, 'robo_script', None): 147 spec.androidRoboTest.roboScript = self._BuildFileReference( 148 os.path.basename(self._args.robo_script)) 149 return spec 150 151 def _BuildAndroidGameLoopTestSpec(self): 152 """Build a TestSpecification for an AndroidTestLoop.""" 153 spec = self._BuildGenericTestSpec() 154 app_apk, app_bundle = self._BuildAppReference(self._args.app) 155 spec.androidTestLoop = self._messages.AndroidTestLoop( 156 appApk=app_apk, 157 appBundle=app_bundle, 158 appPackageId=self._args.app_package) 159 if self._args.scenario_numbers: 160 spec.androidTestLoop.scenarios = self._args.scenario_numbers 161 if self._args.scenario_labels: 162 spec.androidTestLoop.scenarioLabels = self._args.scenario_labels 163 return spec 164 165 def _BuildGenericTestSpec(self): 166 """Build a generic TestSpecification without test-type specifics.""" 167 device_files = [] 168 for obb_file in self._args.obb_files or []: 169 obb_file_name = os.path.basename(obb_file) 170 device_files.append( 171 self._messages.DeviceFile( 172 obbFile=self._messages.ObbFile( 173 obbFileName=obb_file_name, 174 obb=self._BuildFileReference(obb_file_name)))) 175 other_files = getattr(self._args, 'other_files', None) or {} 176 for device_path in other_files.keys(): 177 device_files.append( 178 self._messages.DeviceFile( 179 regularFile=self._messages.RegularFile( 180 content=self._BuildFileReference( 181 util.GetRelativeDevicePath(device_path)), 182 devicePath=device_path))) 183 environment_variables = [] 184 if self._args.environment_variables: 185 for key, value in six.iteritems(self._args.environment_variables): 186 environment_variables.append( 187 self._messages.EnvironmentVariable(key=key, value=value)) 188 189 directories_to_pull = self._args.directories_to_pull or [] 190 191 account = None 192 if self._args.auto_google_login: 193 account = self._messages.Account(googleAuto=self._messages.GoogleAuto()) 194 195 additional_apks = [ 196 self._messages.Apk( 197 location=self._BuildFileReference(os.path.basename(additional_apk))) 198 for additional_apk in getattr(self._args, 'additional_apks', []) or [] 199 ] 200 201 grant_permissions = getattr(self._args, 'grant_permissions', 202 'all') == 'all' 203 204 setup = self._messages.TestSetup( 205 filesToPush=device_files, 206 account=account, 207 environmentVariables=environment_variables, 208 directoriesToPull=directories_to_pull, 209 networkProfile=getattr(self._args, 'network_profile', None), 210 additionalApks=additional_apks, 211 dontAutograntPermissions=not grant_permissions) 212 213 return self._messages.TestSpecification( 214 testTimeout=matrix_ops.ReformatDuration(self._args.timeout), 215 testSetup=setup, 216 disableVideoRecording=not self._args.record_video, 217 disablePerformanceMetrics=not self._args.performance_metrics) 218 219 def _BuildShardingOption(self): 220 """Build a ShardingOption for an AndroidInstrumentationTest.""" 221 if getattr(self._args, 'num_uniform_shards', {}): 222 return self._messages.ShardingOption( 223 uniformSharding=self._messages.UniformSharding( 224 numShards=self._args.num_uniform_shards)) 225 elif getattr(self._args, 'test_targets_for_shard', {}): 226 return self._messages.ShardingOption( 227 manualSharding=self._BuildManualShard( 228 self._args.test_targets_for_shard)) 229 230 def _BuildManualShard(self, test_targets_for_shard): 231 """Build a ManualShard for a ShardingOption.""" 232 test_targets = [ 233 self._BuildTestTargetsForShard(test_target) 234 for test_target in test_targets_for_shard 235 ] 236 return self._messages.ManualSharding(testTargetsForShard=test_targets) 237 238 def _BuildTestTargetsForShard(self, test_targets_for_each_shard): 239 return self._messages.TestTargetsForShard(testTargets=[ 240 target for target in test_targets_for_each_shard.split(';') 241 if target is not None 242 ]) 243 244 def _TestSpecFromType(self, test_type): 245 """Map a test type into its corresponding TestSpecification message.""" 246 if test_type == 'instrumentation': 247 return self._BuildAndroidInstrumentationTestSpec() 248 elif test_type == 'robo': 249 return self._BuildAndroidRoboTestSpec() 250 elif test_type == 'game-loop': 251 return self._BuildAndroidGameLoopTestSpec() 252 else: # It's a bug in our arg validation if we ever get here. 253 raise exceptions.InvalidArgumentException( 254 'type', 'Unknown test type "{}".'.format(test_type)) 255 256 def _BuildTestMatrix(self, spec): 257 """Build just the user-specified parts of a TestMatrix message. 258 259 Args: 260 spec: a TestSpecification message corresponding to the test type. 261 262 Returns: 263 A TestMatrix message. 264 """ 265 if self._args.device: 266 devices = [self._BuildAndroidDevice(d) for d in self._args.device] 267 environment_matrix = self._messages.EnvironmentMatrix( 268 androidDeviceList=self._messages.AndroidDeviceList( 269 androidDevices=devices)) 270 else: 271 environment_matrix = self._messages.EnvironmentMatrix( 272 androidMatrix=self._messages.AndroidMatrix( 273 androidModelIds=self._args.device_ids, 274 androidVersionIds=self._args.os_version_ids, 275 locales=self._args.locales, 276 orientations=self._args.orientations)) 277 278 gcs = self._messages.GoogleCloudStorage(gcsPath=self._gcs_results_root) 279 hist = self._messages.ToolResultsHistory(projectId=self._project, 280 historyId=self._history_id) 281 results = self._messages.ResultStorage(googleCloudStorage=gcs, 282 toolResultsHistory=hist) 283 284 client_info = matrix_creator_common.BuildClientInfo( 285 self._messages, 286 getattr(self._args, 'client_details', {}) or {}, self._release_track) 287 288 return self._messages.TestMatrix( 289 testSpecification=spec, 290 environmentMatrix=environment_matrix, 291 clientInfo=client_info, 292 resultStorage=results, 293 flakyTestAttempts=self._args.num_flaky_test_attempts or 0) 294 295 def _BuildAndroidDevice(self, device_map): 296 return self._messages.AndroidDevice( 297 androidModelId=device_map['model'], 298 androidVersionId=device_map['version'], 299 locale=device_map['locale'], 300 orientation=device_map['orientation']) 301 302 def _BuildTestMatrixRequest(self, request_id): 303 """Build a TestingProjectsTestMatricesCreateRequest for a test matrix. 304 305 Args: 306 request_id: {str} a unique ID for the CreateTestMatrixRequest. 307 308 Returns: 309 A TestingProjectsTestMatricesCreateRequest message. 310 """ 311 spec = self._TestSpecFromType(self._args.type) 312 return self._messages.TestingProjectsTestMatricesCreateRequest( 313 projectId=self._project, 314 testMatrix=self._BuildTestMatrix(spec), 315 requestId=request_id) 316 317 def CreateTestMatrix(self, request_id): 318 """Invoke the Testing service to create a test matrix from the user's args. 319 320 Args: 321 request_id: {str} a unique ID for the CreateTestMatrixRequest. 322 323 Returns: 324 The TestMatrix response message from the TestMatrices.Create rpc. 325 326 Raises: 327 HttpException if the test service reports an HttpError. 328 """ 329 request = self._BuildTestMatrixRequest(request_id) 330 log.debug('TestMatrices.Create request:\n{0}\n'.format(request)) 331 try: 332 response = self._client.projects_testMatrices.Create(request) 333 log.debug('TestMatrices.Create response:\n{0}\n'.format(response)) 334 except apitools_exceptions.HttpError as error: 335 msg = 'Http error while creating test matrix: ' + util.GetError(error) 336 raise exceptions.HttpException(msg) 337 338 log.status.Print('Test [{id}] has been created in the Google Cloud.' 339 .format(id=response.testMatrixId)) 340 return response 341