1# -*- coding: utf-8 -*- # 2# Copyright 2018 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 iOS 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 32 33 34def CreateMatrix(args, context, history_id, gcs_results_root, release_track): 35 """Creates a new iOS matrix test in Firebase Test Lab from the user's params. 36 37 Args: 38 args: an argparse namespace. All the arguments that were provided to this 39 gcloud command invocation (i.e. group and command arguments combined). 40 context: {str:obj} dict containing the gcloud command context, which 41 includes the Testing API client+messages libs generated by Apitools. 42 history_id: {str} A history ID to publish Tool Results to. 43 gcs_results_root: the root dir for a matrix within the GCS results bucket. 44 release_track: the release track that the command is invoked from. 45 46 Returns: 47 A TestMatrix object created from the supplied matrix configuration values. 48 """ 49 creator = MatrixCreator(args, context, history_id, gcs_results_root, 50 release_track) 51 return creator.CreateTestMatrix(uuid.uuid4().hex) 52 53 54class MatrixCreator(object): 55 """Creates a single iOS test matrix based on user-supplied test arguments.""" 56 57 def __init__(self, args, context, history_id, gcs_results_root, 58 release_track): 59 """Construct an MatrixCreator to be used to create a single test matrix. 60 61 Args: 62 args: an argparse namespace. All the arguments that were provided to this 63 gcloud command invocation (i.e. group and command arguments combined). 64 context: {str:obj} dict containing the gcloud command context, which 65 includes the Testing API client+messages libs generated by Apitools. 66 history_id: {str} A history ID to publish Tool Results to. 67 gcs_results_root: the root dir for a matrix within the GCS results bucket. 68 release_track: the release track that the command is invoked from. 69 """ 70 self._project = util.GetProject() 71 self._args = args 72 self._history_id = history_id 73 self._gcs_results_root = gcs_results_root 74 self._client = context['testing_client'] 75 self._messages = context['testing_messages'] 76 self._release_track = release_track 77 78 def _BuildFileReference(self, filename, use_basename=True): 79 """Build a FileReference pointing to a file in GCS.""" 80 if not filename: 81 return None 82 if use_basename: 83 filename = os.path.basename(filename) 84 path = os.path.join(self._gcs_results_root, filename) 85 return self._messages.FileReference(gcsPath=path) 86 87 def _BuildGenericTestSetup(self): 88 """Build an IosTestSetup for an iOS test.""" 89 additional_ipas = [ 90 self._BuildFileReference(os.path.basename(additional_ipa)) 91 for additional_ipa in getattr(self._args, 'additional_ipas', []) or [] 92 ] 93 directories_to_pull = [] 94 for directory in getattr(self._args, 'directories_to_pull', []) or []: 95 if ':' in directory: 96 bundle, path = directory.split(':') 97 directories_to_pull.append( 98 self._messages.IosDeviceFile(bundleId=bundle, devicePath=path)) 99 else: 100 directories_to_pull.append( 101 self._messages.IosDeviceFile(devicePath=directory)) 102 device_files = [] 103 other_files = getattr(self._args, 'other_files', None) or {} 104 for device_path in other_files.keys(): 105 # Device paths are be prefixed by the bundle ID if they refer to an app's 106 # sandboxed filesystem, separated with the device path by ':' 107 idx = device_path.find(':') 108 bundle_id = device_path[:idx] if idx != -1 else None 109 path = device_path[idx + 1:] if idx != -1 else device_path 110 device_files.append( 111 self._messages.IosDeviceFile( 112 content=self._BuildFileReference( 113 util.GetRelativeDevicePath(path), use_basename=False), 114 bundleId=bundle_id, 115 devicePath=path)) 116 return self._messages.IosTestSetup( 117 networkProfile=getattr(self._args, 'network_profile', None), 118 additionalIpas=additional_ipas, 119 pushFiles=device_files, 120 pullDirectories=directories_to_pull) 121 122 def _BuildIosXcTestSpec(self): 123 """Build a TestSpecification for an IosXcTest.""" 124 spec = self._messages.TestSpecification( 125 disableVideoRecording=not self._args.record_video, 126 iosTestSetup=self._BuildGenericTestSetup(), 127 testTimeout=matrix_ops.ReformatDuration(self._args.timeout), 128 iosXcTest=self._messages.IosXcTest( 129 testsZip=self._BuildFileReference(self._args.test), 130 xctestrun=self._BuildFileReference(self._args.xctestrun_file), 131 xcodeVersion=self._args.xcode_version, 132 testSpecialEntitlements=getattr(self._args, 133 'test_special_entitlements', 134 False))) 135 return spec 136 137 def _BuildIosTestLoopTestSpec(self): 138 """Build a TestSpecification for an IosXcTest.""" 139 spec = self._messages.TestSpecification( 140 disableVideoRecording=not self._args.record_video, 141 iosTestSetup=self._BuildGenericTestSetup(), 142 testTimeout=matrix_ops.ReformatDuration(self._args.timeout), 143 iosTestLoop=self._messages.IosTestLoop( 144 appIpa=self._BuildFileReference(self._args.app), 145 scenarios=self._args.scenario_numbers)) 146 return spec 147 148 def _TestSpecFromType(self, test_type): 149 """Map a test type into its corresponding TestSpecification message .""" 150 if test_type == 'xctest': 151 return self._BuildIosXcTestSpec() 152 elif test_type == 'game-loop': 153 return self._BuildIosTestLoopTestSpec() 154 else: # It's a bug in our arg validation if we ever get here. 155 raise exceptions.InvalidArgumentException( 156 'type', 'Unknown test type "{}".'.format(test_type)) 157 158 def _BuildTestMatrix(self, spec): 159 """Build just the user-specified parts of an iOS TestMatrix message. 160 161 Args: 162 spec: a TestSpecification message corresponding to the test type. 163 164 Returns: 165 A TestMatrix message. 166 """ 167 devices = [self._BuildIosDevice(d) for d in self._args.device] 168 environment_matrix = self._messages.EnvironmentMatrix( 169 iosDeviceList=self._messages.IosDeviceList(iosDevices=devices)) 170 171 gcs = self._messages.GoogleCloudStorage(gcsPath=self._gcs_results_root) 172 hist = self._messages.ToolResultsHistory(projectId=self._project, 173 historyId=self._history_id) 174 results = self._messages.ResultStorage(googleCloudStorage=gcs, 175 toolResultsHistory=hist) 176 177 client_info = matrix_creator_common.BuildClientInfo( 178 self._messages, 179 getattr(self._args, 'client_details', {}) or {}, self._release_track) 180 181 return self._messages.TestMatrix( 182 testSpecification=spec, 183 environmentMatrix=environment_matrix, 184 clientInfo=client_info, 185 resultStorage=results, 186 flakyTestAttempts=self._args.num_flaky_test_attempts or 0) 187 188 def _BuildIosDevice(self, device_map): 189 return self._messages.IosDevice( 190 iosModelId=device_map['model'], 191 iosVersionId=device_map['version'], 192 locale=device_map['locale'], 193 orientation=device_map['orientation']) 194 195 def _BuildTestMatrixRequest(self, request_id): 196 """Build a TestingProjectsTestMatricesCreateRequest for a test matrix. 197 198 Args: 199 request_id: {str} a unique ID for the CreateTestMatrixRequest. 200 201 Returns: 202 A TestingProjectsTestMatricesCreateRequest message. 203 """ 204 spec = self._TestSpecFromType(self._args.type) 205 return self._messages.TestingProjectsTestMatricesCreateRequest( 206 projectId=self._project, 207 testMatrix=self._BuildTestMatrix(spec), 208 requestId=request_id) 209 210 def CreateTestMatrix(self, request_id): 211 """Invoke the Testing service to create a test matrix from the user's args. 212 213 Args: 214 request_id: {str} a unique ID for the CreateTestMatrixRequest. 215 216 Returns: 217 The TestMatrix response message from the TestMatrices.Create rpc. 218 219 Raises: 220 HttpException if the test service reports an HttpError. 221 """ 222 request = self._BuildTestMatrixRequest(request_id) 223 log.debug('TestMatrices.Create request:\n{0}\n'.format(request)) 224 try: 225 response = self._client.projects_testMatrices.Create(request) 226 log.debug('TestMatrices.Create response:\n{0}\n'.format(response)) 227 except apitools_exceptions.HttpError as error: 228 msg = 'Http error while creating test matrix: ' + util.GetError(error) 229 raise exceptions.HttpException(msg) 230 231 log.status.Print('Test [{id}] has been created in the Google Cloud.' 232 .format(id=response.testMatrixId)) 233 return response 234