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"""A shared library to support implementation of Firebase Test Lab commands.""" 17 18from __future__ import absolute_import 19from __future__ import division 20from __future__ import unicode_literals 21 22import json 23 24from apitools.base.py import exceptions as apitools_exceptions 25 26from googlecloudsdk.api_lib.firebase.test import exceptions 27from googlecloudsdk.api_lib.util import apis 28from googlecloudsdk.calliope import exceptions as calliope_exceptions 29from googlecloudsdk.core import log 30from googlecloudsdk.core import properties 31 32OUTCOMES_FORMAT = """ 33 table[box]( 34 outcome.color(red=Fail, green=Pass, blue=Flaky, yellow=Inconclusive), 35 axis_value:label=TEST_AXIS_VALUE, 36 test_details:label=TEST_DETAILS 37 ) 38""" 39 40 41def GetError(error): 42 """Returns a ready-to-print string representation from the http response. 43 44 Args: 45 error: the Http error response, whose content is a JSON-format string for 46 most cases (e.g. invalid test dimension), but can be just a string other 47 times (e.g. invalid URI for CLOUDSDK_TEST_ENDPOINT). 48 49 Returns: 50 A ready-to-print string representation of the error. 51 """ 52 try: 53 data = json.loads(error.content) 54 except ValueError: # message is not JSON 55 return error.content 56 57 code = data['error']['code'] 58 message = data['error']['message'] 59 return 'ResponseError {0}: {1}'.format(code, message) 60 61 62def GetErrorCodeAndMessage(error): 63 """Returns the individual error code and message from a JSON http response. 64 65 Prefer using GetError(error) unless you need to examine the error code and 66 take specific action on it. 67 68 Args: 69 error: the Http error response, whose content is a JSON-format string. 70 71 Returns: 72 (code, msg) A tuple holding the error code and error message string. 73 74 Raises: 75 ValueError: if the error is not in JSON format. 76 """ 77 data = json.loads(error.content) 78 return data['error']['code'], data['error']['message'] 79 80 81def GetProject(): 82 """Get the user's project id from the core project properties. 83 84 Returns: 85 The id of the GCE project to use while running the test. 86 87 Raises: 88 MissingProjectError: if the user did not specify a project id via the 89 --project flag or via running "gcloud config set project PROJECT_ID". 90 """ 91 project = properties.VALUES.core.project.Get() 92 if not project: 93 raise exceptions.MissingProjectError( 94 'No project specified. Please add --project PROJECT_ID to the command' 95 ' line or first run\n $ gcloud config set project PROJECT_ID') 96 return project 97 98 99def GetDeviceIpBlocks(context=None): 100 """Gets the device IP block catalog from the TestEnvironmentDiscoveryService. 101 102 Args: 103 context: {str:object}, The current context, which is a set of key-value 104 pairs that can be used for common initialization among commands. 105 106 Returns: 107 The device IP block catalog 108 109 Raises: 110 calliope_exceptions.HttpException: If it could not connect to the service. 111 """ 112 if context: 113 client = context['testing_client'] 114 messages = context['testing_messages'] 115 else: 116 client = apis.GetClientInstance('testing', 'v1') 117 messages = apis.GetMessagesModule('testing', 'v1') 118 119 env_type = ( 120 messages.TestingTestEnvironmentCatalogGetRequest 121 .EnvironmentTypeValueValuesEnum.DEVICE_IP_BLOCKS) 122 return _GetCatalog(client, messages, env_type).deviceIpBlockCatalog 123 124 125def GetAndroidCatalog(context=None): 126 """Gets the Android catalog from the TestEnvironmentDiscoveryService. 127 128 Args: 129 context: {str:object}, The current context, which is a set of key-value 130 pairs that can be used for common initialization among commands. 131 132 Returns: 133 The android catalog. 134 135 Raises: 136 calliope_exceptions.HttpException: If it could not connect to the service. 137 """ 138 if context: 139 client = context['testing_client'] 140 messages = context['testing_messages'] 141 else: 142 client = apis.GetClientInstance('testing', 'v1') 143 messages = apis.GetMessagesModule('testing', 'v1') 144 145 env_type = ( 146 messages.TestingTestEnvironmentCatalogGetRequest. 147 EnvironmentTypeValueValuesEnum.ANDROID) 148 return _GetCatalog(client, messages, env_type).androidDeviceCatalog 149 150 151def GetIosCatalog(context=None): 152 """Gets the iOS catalog from the TestEnvironmentDiscoveryService. 153 154 Args: 155 context: {str:object}, The current context, which is a set of key-value 156 pairs that can be used for common initialization among commands. 157 158 Returns: 159 The iOS catalog. 160 161 Raises: 162 calliope_exceptions.HttpException: If it could not connect to the service. 163 """ 164 if context: 165 client = context['testing_client'] 166 messages = context['testing_messages'] 167 else: 168 client = apis.GetClientInstance('testing', 'v1') 169 messages = apis.GetMessagesModule('testing', 'v1') 170 171 env_type = ( 172 messages.TestingTestEnvironmentCatalogGetRequest. 173 EnvironmentTypeValueValuesEnum.IOS) 174 return _GetCatalog(client, messages, env_type).iosDeviceCatalog 175 176 177def GetNetworkProfileCatalog(context=None): 178 """Gets the network profile catalog from the TestEnvironmentDiscoveryService. 179 180 Args: 181 context: {str:object}, The current context, which is a set of key-value 182 pairs that can be used for common initialization among commands. 183 184 Returns: 185 The network profile catalog. 186 187 Raises: 188 calliope_exceptions.HttpException: If it could not connect to the service. 189 """ 190 if context: 191 client = context['testing_client'] 192 messages = context['testing_messages'] 193 else: 194 client = apis.GetClientInstance('testing', 'v1') 195 messages = apis.GetMessagesModule('testing', 'v1') 196 197 env_type = ( 198 messages.TestingTestEnvironmentCatalogGetRequest. 199 EnvironmentTypeValueValuesEnum.NETWORK_CONFIGURATION) 200 return _GetCatalog(client, messages, env_type).networkConfigurationCatalog 201 202 203def _GetCatalog(client, messages, environment_type): 204 """Gets a test environment catalog from the TestEnvironmentDiscoveryService. 205 206 Args: 207 client: The Testing API client object. 208 messages: The Testing API messages object. 209 environment_type: {enum} which EnvironmentType catalog to get. 210 211 Returns: 212 The test environment catalog. 213 214 Raises: 215 calliope_exceptions.HttpException: If it could not connect to the service. 216 """ 217 project_id = properties.VALUES.core.project.Get() 218 request = messages.TestingTestEnvironmentCatalogGetRequest( 219 environmentType=environment_type, 220 projectId=project_id) 221 try: 222 return client.testEnvironmentCatalog.Get(request) 223 except apitools_exceptions.HttpError as error: 224 raise calliope_exceptions.HttpException( 225 'Unable to access the test environment catalog: ' + GetError(error)) 226 except: 227 # Give the user some explanation in case we get a vague/unexpected error, 228 # such as a socket.error from httplib2. 229 log.error('Unable to access the Firebase Test Lab environment catalog.') 230 raise # Re-raise the error in case Calliope can do something with it. 231 232 233def ParseRoboDirectiveKey(key): 234 """Returns a tuple representing a directive's type and resource name. 235 236 Args: 237 key: the directive key, which can be "<type>:<resource>" or "<resource>" 238 239 Returns: 240 A tuple of the directive's parsed type and resource name. If no type is 241 specified, "text" will be returned as the default type. 242 243 Raises: 244 InvalidArgException: if the input format is incorrect or if the specified 245 type is unsupported. 246 """ 247 248 parts = key.split(':') 249 resource_name = parts[-1] 250 if len(parts) > 2: 251 # Invalid format: at most one ':' is allowed. 252 raise exceptions.InvalidArgException( 253 'robo_directives', 'Invalid format for key [{0}]. ' 254 'Use a colon only to separate action type and resource name.'.format( 255 key)) 256 257 if len(parts) == 1: 258 # Format: '<resource_name>=<input_text>' defaults to 'text' 259 action_type = 'text' 260 else: 261 # Format: '<type>:<resource_name>=<input_value>' 262 action_type = parts[0] 263 supported_action_types = ['text', 'click', 'ignore'] 264 if action_type not in supported_action_types: 265 raise exceptions.InvalidArgException( 266 'robo_directives', 267 'Unsupported action type [{0}]. Please choose one of [{1}]'.format( 268 action_type, ', '.join(supported_action_types))) 269 270 return (action_type, resource_name) 271 272 273def GetDeprecatedTagWarning(models): 274 """Returns a warning string iff any device model is marked deprecated.""" 275 for model in models: 276 for tag in model.tags: 277 if 'deprecated' in tag: 278 return ('Some devices are deprecated. Learn more at https://firebase.' 279 'google.com/docs/test-lab/available-testing-devices#deprecated') 280 return None 281 282 283def GetRelativeDevicePath(device_path): 284 """Returns the relative device path that can be joined with GCS bucket.""" 285 return device_path[1:] if device_path.startswith('/') else device_path 286