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