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