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