fastlane-plugin-firebase_test_lab 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0a8802e9a08e68e912d7c73f2c36bc68e97d27756f7495449bc076d2810c7381
4
+ data.tar.gz: ec28f88fd8eb81965dba501ce0e9cf8e11a24f7ddd7ddf4f82d3cb883397b768
5
+ SHA512:
6
+ metadata.gz: 5e1230cffef6c6498779f9c6a41c93bf34b1144f068a3c0c8a1b7bc4fd1bb9485545640683c1fb2b26c90a4668e9d4fefe8250851eb9e7e9e7ac29877f5a33a7
7
+ data.tar.gz: 1760eccb32955a7451a2b70c1c4d2f870a3801db7db3f62547d7464d65dc9376cd96a75ff579efe75e5a2572fb8835f7b27f0cb0021a0d7fbca7dc2f1bc9c8e6
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-present the fastlane authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1,99 @@
1
+ # `Firebase Test Lab` Plugin for fastlane
2
+
3
+ This project is a [fastlane](https://fastlane.tools) plugin. You can add it to your [fastlane](https://fastlane.tools) project by running
4
+
5
+ ```bash
6
+ fastlane add_plugin firebase_test_lab
7
+ ```
8
+
9
+ ## About Firebase Test Lab plugin
10
+
11
+ [Firebase Test Lab](https://firebase.google.com/docs/test-lab/) let you easily test your iOS and Android app on a variety of real or virtual devices and configurations with just one API call. This plugin allows you to submit your app to Firebase Test Lab by adding an action into Fastfile.
12
+
13
+ ## Getting started
14
+
15
+ ### If you are not current user of Firebase
16
+ You need to set up Firebase first. These only needs to be done once for an organization.
17
+
18
+ - If you have not used Google Cloud before, you need to [create a new Google Cloud project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#Creating%20a%20Project) first.
19
+ - Go to the [Firebase Console](https://console.firebase.google.com/). Add Firebase into your Google Cloud project by clicking on "Add project" and then choose your just-created project..
20
+
21
+ ### Configure Google credentials through service accounts
22
+ To authenticate, Google Cloud credentials will need to be set for any machine where fastlane and this plugin runs on.
23
+
24
+ If you are running this plugin on Google Cloud [Compute Engine](https://cloud.google.com/compute), [Kubernetes Engine](https://cloud.google.com/kubernetes-engine) or [App Engine flexible environment](https://cloud.google.com/appengine/docs/flexible/), a default service account is automatically provisioned. You will not need to create a service account. See [this](https://cloud.google.com/compute/docs/access/service-accounts#compute_engine_default_service_account) for more details.
25
+
26
+ In all other cases, you would need to configure the service account manually. You can follow [this guide](https://cloud.google.com/docs/authentication/getting-started) on how to create a new service account and create a key for it. You will need to set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable pointing to the service account key file according to the document.
27
+
28
+ No matter you are a using an automatically provisioned service account or a manually created one, the service account must be configured to have project editor role.
29
+
30
+ ### Enable relevant Google APIs
31
+ - You need to enable the following APIs on your [Google Cloud API library](https://console.cloud.google.com/apis/library) (see [this](https://support.google.com/cloud/answer/6158841) for how):
32
+ 1. Cloud Testing API
33
+ 2. Cloud Tool Results API
34
+
35
+ ### Find out the devices you want to test on
36
+ If you have [gcloud tool](https://cloud.google.com/sdk/gcloud/), you can run
37
+
38
+ ```bash
39
+ gcloud beta firebase test ios models list
40
+ ```
41
+ This will return a list of supported devices and their identifiers.
42
+
43
+ All available devices can also be seen [here](https://firebase.google.com/docs/test-lab/ios/available-testing-devices).
44
+
45
+
46
+ ## Actions
47
+
48
+ ### firebase_test_lab_ios_xctest
49
+
50
+ Submit your iOS app to Firebase Test Lab and run XCTest. Refer to [this document](https://firebase.google.com/docs/test-lab/ios/command-line) for more details about Firebase Test Lab specific arguments.
51
+ ```ruby
52
+ scan(
53
+ scheme: 'YourApp', # XCTest scheme
54
+ clean: true, # Recommended: This would ensure the build would not include unnecessary files
55
+ skip_detect_devices: true, # Required
56
+ build_for_testing: true, # Required
57
+ sdk: 'iphoneos', # Required
58
+ should_zip_build_products: true # Must be true to set the correct format for Firebase Test Lab
59
+ )
60
+ firebase_test_lab_ios_xctest(
61
+ gcp_project: 'your-google-project', # Your Google Cloud project name
62
+ devices: [ # Device(s) to run tests on
63
+ {
64
+ ios_model_id: 'iphonex', # Device model ID, see gcloud command above
65
+ ios_version_id: '11.2', # iOS version ID, see gcloud command above
66
+ locale: 'en_US', # Optional: default to en_US if not set
67
+ orientation: 'portrait' # Optional: default to portrait if not set
68
+ }
69
+ ]
70
+ )
71
+ ```
72
+
73
+ Arguments available are:
74
+
75
+ - `app_path` You may provide a different path in the local filesystem (e.g: `/path/to/app-bundle.zip`) or on Google Cloud Storage (`gs://your-bucket/path/to/app-bundle.zip`) that points to an app bundle as specified [here](https://firebase.google.com/docs/test-lab/ios/command-line#build_xctests_for_your_app). If a Google Cloud Storage path is used, the service account must have read access to such file.
76
+ - `gcp_project` The Google Cloud project name for Firebase Test Lab to run on.
77
+ - `oauth_key_file_path` The path to the Google Cloud service account key. If not set, the default credential will be used.
78
+ - `devices` An array of devices for your app to be tested on. Each device is represented as a hash, with ios_model_id, ios_version_id, locale and orientation properties, the first two of which are required. If not set, it will be defaulted to iPhone X on iOS 11.2. This array cannot be empty.
79
+ - `async` If set to true, the action will not wait for the test results but exit immediately.
80
+ - `timeout_sec` After how long will the test be abandoned by Firebase Test Lab. Duration hould be given in seconds.
81
+ - `result_storage` Designate which location on Google Cloud Storage to store the test results. This should be a directory (e.g: `gs://your-bucket/tests/`)
82
+
83
+ ## Issues and Feedback
84
+
85
+ If you have any other issues and feedback about this plugin, we appreciate if you could submit an issue to this repository.
86
+
87
+ You can also join the Firebase slack channel [here](https://firebase.community/).
88
+
89
+ ## Troubleshooting
90
+
91
+ For some more detailed help with plugins problems, check out the [Plugins Troubleshooting](https://github.com/fastlane/fastlane/blob/master/fastlane/docs/PluginsTroubleshooting.md) doc in the main `fastlane` repo.
92
+
93
+ ## Using `fastlane` Plugins
94
+
95
+ For more information about how the `fastlane` plugin system works, check out the [Plugins documentation](https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Plugins.md) in the main `fastlane` repo.
96
+
97
+ ## About `fastlane`
98
+
99
+ `fastlane` automates building, testing, and releasing your app for beta and app store distributions. To learn more about `fastlane`, check out [fastlane.tools](https://fastlane.tools).
@@ -0,0 +1,16 @@
1
+ require 'fastlane/plugin/firebase_test_lab/module'
2
+
3
+ module Fastlane
4
+ module FirebaseTestLab
5
+ # Return all .rb files inside the "actions" and "helper" directory
6
+ def self.all_classes
7
+ Dir[File.expand_path('*/{actions,helper}/*.rb', File.dirname(__FILE__))]
8
+ end
9
+ end
10
+ end
11
+
12
+ # By default we want to import all available actions and helpers
13
+ # A plugin can contain any number of actions and plugins
14
+ Fastlane::FirebaseTestLab.all_classes.each do |current|
15
+ require current
16
+ end
@@ -0,0 +1,268 @@
1
+ require_relative '../helper/ftl_service'
2
+ require_relative '../helper/ftl_message'
3
+ require_relative '../helper/storage'
4
+ require_relative '../helper/credential'
5
+ require_relative '../helper/ios_validator'
6
+ require_relative '../options'
7
+
8
+ require 'json'
9
+ require 'securerandom'
10
+ require 'tty-spinner'
11
+
12
+ module Fastlane
13
+ module Actions
14
+ class FirebaseTestLabIosXctestAction < Action
15
+ DEFAULT_APP_BUNDLE_NAME = "bundle"
16
+ PULL_RESULT_INTERVAL = 5
17
+
18
+ RUNNING_STATES = %w(VALIDATING PENDING RUNNING)
19
+
20
+ private_constant :DEFAULT_APP_BUNDLE_NAME
21
+ private_constant :PULL_RESULT_INTERVAL
22
+ private_constant :RUNNING_STATES
23
+
24
+ def self.run(params)
25
+ gcp_project = params[:gcp_project]
26
+ oauth_key_file_path = params[:oauth_key_file_path]
27
+ gcp_credential = Fastlane::FirebaseTestLab::Credential.new(key_file_path: oauth_key_file_path)
28
+
29
+ ftl_service = Fastlane::FirebaseTestLab::FirebaseTestLabService.new(gcp_credential)
30
+
31
+ # The default Google Cloud Storage path we store app bundle and test results
32
+ gcs_workfolder = generate_directory_name
33
+
34
+ # Firebase Test Lab requires an app bundle be already on Google Cloud Storage before starting the job
35
+ if params[:app_path].to_s.start_with?("gs://")
36
+ # gs:// is a path on Google Cloud Storage, we do not need to re-upload the app to a different bucket
37
+ app_gcs_link = params[:app_path]
38
+ else
39
+ FirebaseTestLab::IosValidator.validate_ios_app(params[:app_path])
40
+
41
+ # When given a local path, we upload the app bundle to Google Cloud Storage
42
+ upload_spinner = TTY::Spinner.new("[:spinner] Uploading the app to GCS...", format: :dots)
43
+ upload_spinner.auto_spin
44
+ upload_bucket_name = ftl_service.get_default_bucket(gcp_project)
45
+ app_gcs_link = upload_file(params[:app_path],
46
+ upload_bucket_name,
47
+ "#{gcs_workfolder}/#{DEFAULT_APP_BUNDLE_NAME}",
48
+ gcp_project,
49
+ gcp_credential)
50
+ upload_spinner.success("Done")
51
+ end
52
+
53
+ UI.message("Submitting job(s) to Firebase Test Lab")
54
+ result_storage = (params[:result_storage] ||
55
+ "gs://#{ftl_service.get_default_bucket(gcp_project)}/#{gcs_workfolder}")
56
+
57
+ # We have gathered all the information. Call Firebase Test Lab to start the job now
58
+ matrix_id = ftl_service.start_job(gcp_project,
59
+ app_gcs_link,
60
+ result_storage,
61
+ params[:devices],
62
+ params[:timeout_sec])
63
+
64
+ # In theory, matrix_id should be available. Keep it to catch unexpected Firebase Test Lab API response
65
+ if matrix_id.nil?
66
+ UI.abort_with_message!("No matrix ID received.")
67
+ end
68
+ UI.message("Matrix ID for this submission: #{matrix_id}")
69
+ wait_for_test_results(ftl_service, gcp_project, matrix_id, params[:async])
70
+ end
71
+
72
+ def self.upload_file(app_path, bucket_name, gcs_path, gcp_project, gcp_credential)
73
+ file_name = "gs://#{bucket_name}/#{gcs_path}"
74
+ storage = Fastlane::FirebaseTestLab::Storage.new(gcp_project, gcp_credential)
75
+ storage.upload_file(File.expand_path(app_path), bucket_name, gcs_path)
76
+ return file_name
77
+ end
78
+
79
+ def self.wait_for_test_results(ftl_service, gcp_project, matrix_id, async)
80
+ firebase_console_link = nil
81
+
82
+ spinner = TTY::Spinner.new("[:spinner] Starting tests...", format: :dots)
83
+ spinner.auto_spin
84
+
85
+ # Keep pulling test results until they are ready
86
+ loop do
87
+ results = ftl_service.get_matrix_results(gcp_project, matrix_id)
88
+
89
+ if firebase_console_link.nil?
90
+ history_id, execution_id = try_get_history_id_and_execution_id(results)
91
+ # Once we get the Firebase console link, we display that exactly once
92
+ unless history_id.nil? || execution_id.nil?
93
+ firebase_console_link = "https://console.firebase.google.com" \
94
+ "/project/#{gcp_project}/testlab/histories/#{history_id}/matrices/#{execution_id}"
95
+
96
+ spinner.success("Done")
97
+ UI.message("Go to #{firebase_console_link} for more information about this run")
98
+
99
+ if async
100
+ UI.success("Job(s) have been submitted to Firebase Test Lab")
101
+ return
102
+ end
103
+
104
+ spinner = TTY::Spinner.new("[:spinner] Waiting for results...", format: :dots)
105
+ spinner.auto_spin
106
+ end
107
+ end
108
+
109
+ state = results["state"]
110
+ # Handle all known error statuses
111
+ if FirebaseTestLab::ERROR_STATE_TO_MESSAGE.key?(state.to_sym)
112
+ spinner.error("Failed")
113
+ invalid_matrix_details = results["invalidMatrixDetails"]
114
+ if invalid_matrix_details &&
115
+ FirebaseTestLab::INVALID_MATRIX_DETAIL_TO_MESSAGE.key?(invalid_matrix_details.to_sym)
116
+ UI.error(FirebaseTestLab::INVALID_MATRIX_DETAIL_TO_MESSAGE[invalid_matrix_details.to_sym])
117
+ end
118
+ UI.user_error!(FirebaseTestLab::ERROR_STATE_TO_MESSAGE[state.to_sym])
119
+ end
120
+
121
+ if state == "FINISHED"
122
+ spinner.success("Done")
123
+ # Inspect the execution results: only contain info on whether each job finishes.
124
+ # Do not include whether tests fail
125
+ executions_completed = extract_execution_results(results)
126
+
127
+ if results["resultStorage"].nil? || results["resultStorage"]["toolResultsExecution"].nil?
128
+ UI.abort_with_message!("Unexpected response from Firebase test lab: Cannot retrieve result info")
129
+ end
130
+
131
+ # Now, look at the actual test result and see if they succeed
132
+ history_id, execution_id = try_get_history_id_and_execution_id(results)
133
+ if history_id.nil? || execution_id.nil?
134
+ UI.abort_with_message!("Unexpected response from Firebase test lab: No history or execution ID")
135
+ end
136
+ test_results = ftl_service.get_execution_steps(gcp_project, history_id, execution_id)
137
+ tests_successful = extract_test_results(test_results, gcp_project, history_id, execution_id)
138
+ unless executions_completed && tests_successful
139
+ UI.test_failure!("Tests failed")
140
+ end
141
+ return
142
+ end
143
+
144
+ # We should have caught all known states here. If the state is not one of them, this
145
+ # plugin should be modified to handle that
146
+ unless RUNNING_STATES.include?(state)
147
+ spinner.error("Failed")
148
+ UI.abort_with_message!("The test execution is in an unknown state: #{state}. " \
149
+ "We appreciate if you could notify us at " \
150
+ "https://github.com/fastlane/fastlane-plugin-firebase_test_lab/issues")
151
+ end
152
+ sleep(PULL_RESULT_INTERVAL)
153
+ end
154
+ end
155
+
156
+ def self.generate_directory_name
157
+ timestamp = Time.now.getutc.strftime("%Y%m%d-%H%M%SZ")
158
+ return "fastlane-#{timestamp}-#{SecureRandom.hex[0..5]}"
159
+ end
160
+
161
+ def self.try_get_history_id_and_execution_id(matrix_results)
162
+ if matrix_results["resultStorage"].nil? || matrix_results["resultStorage"]["toolResultsExecution"].nil?
163
+ return nil, nil
164
+ end
165
+
166
+ tool_results_execution = matrix_results["resultStorage"]["toolResultsExecution"]
167
+ history_id = tool_results_execution["historyId"]
168
+ execution_id = tool_results_execution["executionId"]
169
+ return history_id, execution_id
170
+ end
171
+
172
+ def self.extract_execution_results(execution_results)
173
+ UI.message("Test job(s) are finalized")
174
+ UI.message("-------------------------")
175
+ UI.message("| EXECUTION RESULTS |")
176
+ failures = 0
177
+ execution_results["testExecutions"].each do |execution|
178
+ UI.message("-------------------------")
179
+ execution_info = "#{execution['id']}: #{execution['state']}"
180
+ if execution["state"] != "FINISHED"
181
+ failures += 1
182
+ UI.error(execution_info)
183
+ else
184
+ UI.success(execution_info)
185
+ end
186
+
187
+ # Display build logs
188
+ if !execution["testDetails"].nil? && !execution["testDetails"]["progressMessages"].nil?
189
+ execution["testDetails"]["progressMessages"].each { |msg| UI.message(msg) }
190
+ end
191
+ end
192
+
193
+ UI.message("-------------------------")
194
+ if failures > 0
195
+ UI.error("😞 #{failures} execution(s) have failed to complete.")
196
+ else
197
+ UI.success("🎉 All jobs have ran and completed.")
198
+ end
199
+ return failures == 0
200
+ end
201
+
202
+ def self.extract_test_results(test_results, gcp_project, history_id, execution_id)
203
+ steps = test_results["steps"]
204
+ failures = 0
205
+ inconclusive_runs = 0
206
+
207
+ UI.message("-------------------------")
208
+ UI.message("| TEST OUTCOME |")
209
+ steps.each do |step|
210
+ UI.message("-------------------------")
211
+ step_id = step["stepId"]
212
+ UI.message("Test step: #{step_id}")
213
+
214
+ run_duration_sec = step["runDuration"]["seconds"] || 0
215
+ UI.message("Execution time: #{run_duration_sec} seconds")
216
+
217
+ outcome = step["outcome"]["summary"]
218
+ case outcome
219
+ when "success"
220
+ UI.success("Result: #{outcome}")
221
+ when "skipped"
222
+ UI.message("Result: #{outcome}")
223
+ when "inconclusive"
224
+ inconclusive_runs += 1
225
+ UI.error("Result: #{outcome}")
226
+ when "failure"
227
+ failures += 1
228
+ UI.error("Result: #{outcome}")
229
+ end
230
+ UI.message("For details, go to https://console.firebase.google.com/project/#{gcp_project}/testlab/" \
231
+ "histories/#{history_id}/matrices/#{execution_id}/executions/#{step_id}")
232
+ end
233
+
234
+ UI.message("-------------------------")
235
+ if failures == 0 && inconclusive_runs == 0
236
+ UI.success("🎉 Yay! All executions are completed successfully!")
237
+ end
238
+ if failures > 0
239
+ UI.error("😞 #{failures} step(s) have failed.")
240
+ end
241
+ if inconclusive_runs > 0
242
+ UI.error("😞 #{inconclusive_runs} step(s) yielded inconclusive outcomes.")
243
+ end
244
+ return failures == 0 && inconclusive_runs == 0
245
+ end
246
+
247
+ #####################################################
248
+ # @!group Documentation
249
+ #####################################################
250
+
251
+ def self.description
252
+ "Submit an iOS XCTest job to Firebase Test Lab"
253
+ end
254
+
255
+ def self.available_options
256
+ Fastlane::FirebaseTestLab::Options.available_options
257
+ end
258
+
259
+ def self.authors
260
+ ["powerivq"]
261
+ end
262
+
263
+ def self.is_supported?(platform)
264
+ return platform == :ios
265
+ end
266
+ end
267
+ end
268
+ end
@@ -0,0 +1,33 @@
1
+ require 'googleauth'
2
+
3
+ module Fastlane
4
+ module FirebaseTestLab
5
+ class Credential
6
+ def initialize(key_file_path: nil)
7
+ @key_file_path = key_file_path
8
+ end
9
+
10
+ def get_google_credential(scopes)
11
+ unless @key_file_path
12
+ begin
13
+ return Google::Auth.get_application_default(scopes)
14
+ rescue => ex
15
+ UI.abort_with_message!("Failed reading application default credential. Either the Oauth credential should be provided or Google Application Default Credential should be configured: #{ex.message}")
16
+ end
17
+ end
18
+
19
+ File.open(File.expand_path(@key_file_path), "r") do |file|
20
+ options = {
21
+ json_key_io: file,
22
+ scope: scopes
23
+ }
24
+ begin
25
+ return Google::Auth::ServiceAccountCredentials.make_creds(options)
26
+ rescue => ex
27
+ UI.abort_with_message!("Failed reading OAuth credential: #{ex.message}")
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ require 'json'
2
+
3
+ module Fastlane
4
+ module FirebaseTestLab
5
+ class ErrorHelper
6
+ def self.summarize_google_error(payload)
7
+ begin
8
+ response = JSON.parse(payload)
9
+ rescue JSON::ParserError => ex
10
+ FastlaneCore::UI.error("Unable to parse error message: #{ex.class}, message: #{ex.message}")
11
+ return payload
12
+ end
13
+
14
+ if response["error"]
15
+ return "#{response['error']['message']}\n#{payload}"
16
+ end
17
+ return payload
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,51 @@
1
+ module Fastlane
2
+ module FirebaseTestLab
3
+ ERROR_STATE_TO_MESSAGE = {
4
+ ERROR: "The execution or matrix has stopped because it encountered an infrastructure failure.",
5
+ UNSUPPORTED_ENVIRONMENT: "The execution was not run because it corresponds to a unsupported environment.",
6
+ INCOMPATIBLE_ENVIRONMENT: "The execution was not run because the provided inputs are incompatible with the " \
7
+ "requested environment",
8
+ INCOMPATIBLE_ARCHITECTURE: "The execution was not run because the provided inputs are incompatible with the " \
9
+ "requested architecture.",
10
+ CANCELLED: "The user cancelled the execution.",
11
+ INVALID: "The execution or matrix was not run because the provided inputs are not valid."
12
+ }
13
+
14
+ INVALID_MATRIX_DETAIL_TO_MESSAGE = {
15
+ MALFORMED_APK: "The app APK is not a valid Android application",
16
+ MALFORMED_TEST_APK: "The test APK is not a valid Android instrumentation test",
17
+ NO_MANIFEST: "The app APK is missing the manifest file",
18
+ NO_PACKAGE_NAME: "The APK manifest file is missing the package name",
19
+ TEST_SAME_AS_APP: "The test APK is the same as the app APK",
20
+ NO_INSTRUMENTATION: "The test APK declares no instrumentation tags in the manifest",
21
+ NO_SIGNATURE: "At least one supplied APK file has a missing or invalid signature",
22
+ INSTRUMENTATION_ORCHESTRATOR_INCOMPATIBLE: "The test runner class specified by the user or the test APK\"s " \
23
+ "manifest file is not compatible with Android Test Orchestrator. " \
24
+ "Please use AndroidJUnitRunner version 1.0 or higher",
25
+ NO_TEST_RUNNER_CLASS: "The test APK does not contain the test runner class specified by " \
26
+ "the user or the manifest file. The test runner class name may be " \
27
+ "incorrect, or the class may be mislocated in the app APK.",
28
+ NO_LAUNCHER_ACTIVITY: "The app APK does not specify a main launcher activity",
29
+ FORBIDDEN_PERMISSIONS: "The app declares one or more permissions that are not allowed",
30
+ INVALID_ROBO_DIRECTIVES: "Cannot have multiple robo-directives with the same resource name",
31
+ TEST_LOOP_INTENT_FILTER_NOT_FOUND: "The app does not have a correctly formatted game-loop intent filter",
32
+ SCENARIO_LABEL_NOT_DECLARED: "A scenario-label was not declared in the manifest file",
33
+ SCENARIO_LABEL_MALFORMED: "A scenario-label in the manifest includes invalid numbers or ranges",
34
+ SCENARIO_NOT_DECLARED: "A scenario-number was not declared in the manifest file",
35
+ DEVICE_ADMIN_RECEIVER: "Device administrator applications are not allowed",
36
+ MALFORMED_XC_TEST_ZIP: "The XCTest zip file was malformed. The zip did not contain a single " \
37
+ ".xctestrun file and the contents of the DerivedData/Build/Products directory.",
38
+ BUILT_FOR_IOS_SIMULATOR: "The provided XCTest was built for the iOS simulator rather than for " \
39
+ "a physical device",
40
+ NO_TESTS_IN_XC_TEST_ZIP: "The .xctestrun file did not specify any test targets to run",
41
+ USE_DESTINATION_ARTIFACTS: "One or more of the test targets defined in the .xctestrun file " \
42
+ "specifies \"UseDestinationArtifacts\", which is not allowed",
43
+ TEST_NOT_APP_HOSTED: "One or more of the test targets defined in the .xctestrun file " \
44
+ "does not have a host binary to run on the physical iOS device, " \
45
+ "which may cause errors when running xcodebuild",
46
+ NO_CODE_APK: "\"hasCode\" is false in the Manifest. Tested APKs must contain code",
47
+ INVALID_INPUT_APK: "Either the provided input APK path was malformed, the APK file does " \
48
+ "not exist, or the user does not have permission to access the file"
49
+ }
50
+ end
51
+ end
@@ -0,0 +1,202 @@
1
+ require 'googleauth'
2
+ require 'json'
3
+
4
+ require_relative './error_helper'
5
+ require_relative '../module'
6
+
7
+ module Fastlane
8
+ module FirebaseTestLab
9
+ class FirebaseTestLabService
10
+ APIARY_ENDPOINT = "https://www.googleapis.com"
11
+ TOOLRESULTS_GET_SETTINGS_API_V3 = "/toolresults/v1beta3/projects/{project}/settings"
12
+ TOOLRESULTS_INITIALIZE_SETTINGS_API_V3 = "/toolresults/v1beta3/projects/{project}:initializeSettings"
13
+ TOOLRESULTS_LIST_EXECUTION_STEP_API_V3 =
14
+ "/toolresults/v1beta3/projects/{project}/histories/{history_id}/executions/{execution_id}/steps"
15
+
16
+ FIREBASE_TEST_LAB_ENDPOINT = "https://testing.googleapis.com"
17
+ FTL_CREATE_API = "/v1/projects/{project}/testMatrices"
18
+ FTL_RESULTS_API = "/v1/projects/{project}/testMatrices/{matrix}"
19
+
20
+ TESTLAB_OAUTH_SCOPES = ["https://www.googleapis.com/auth/cloud-platform"]
21
+
22
+ private_constant :APIARY_ENDPOINT
23
+ private_constant :TOOLRESULTS_GET_SETTINGS_API_V3
24
+ private_constant :TOOLRESULTS_INITIALIZE_SETTINGS_API_V3
25
+ private_constant :TOOLRESULTS_LIST_EXECUTION_STEP_API_V3
26
+ private_constant :FIREBASE_TEST_LAB_ENDPOINT
27
+ private_constant :FTL_CREATE_API
28
+ private_constant :FTL_RESULTS_API
29
+ private_constant :TESTLAB_OAUTH_SCOPES
30
+
31
+ def initialize(credential)
32
+ @auth = credential.get_google_credential(TESTLAB_OAUTH_SCOPES)
33
+ @default_bucket = nil
34
+ end
35
+
36
+ def init_default_bucket(gcp_project)
37
+ conn = Faraday.new(APIARY_ENDPOINT)
38
+ begin
39
+ conn.post(TOOLRESULTS_INITIALIZE_SETTINGS_API_V3.gsub("{project}", gcp_project)) do |req|
40
+ req.headers = @auth.apply(req.headers)
41
+ req.options.timeout = 15
42
+ req.options.open_timeout = 5
43
+ end
44
+ rescue Faraday::Error => ex
45
+ UI.abort_with_message!("Network error when initializing Firebase Test Lab, " \
46
+ "type: #{ex.class}, message: #{ex.message}")
47
+ end
48
+ end
49
+
50
+ def get_default_bucket(gcp_project)
51
+ return @default_bucket unless @default_bucket.nil?
52
+
53
+ init_default_bucket(gcp_project)
54
+ conn = Faraday.new(APIARY_ENDPOINT)
55
+ begin
56
+ resp = conn.get(TOOLRESULTS_GET_SETTINGS_API_V3.gsub("{project}", gcp_project)) do |req|
57
+ req.headers = @auth.apply(req.headers)
58
+ req.options.timeout = 15
59
+ req.options.open_timeout = 5
60
+ end
61
+ rescue Faraday::Error => ex
62
+ UI.abort_with_message!("Network error when obtaining Firebase Test Lab default GCS bucket, " \
63
+ "type: #{ex.class}, message: #{ex.message}")
64
+ end
65
+
66
+ if resp.status != 200
67
+ FastlaneCore::UI.error("Failed to obtain default bucket for Firebase Test Lab.")
68
+ summarized_error = ErrorHelper.summarize_google_error(resp.body)
69
+ if summarized_error.include?("Not Authorized for project")
70
+ FastlaneCore::UI.error("Please make sure that the account associated with your Google credential is the " \
71
+ "project editor or owner. You can do this at the Google Developer Console " \
72
+ "https://console.cloud.google.com/iam-admin/iam?project=#{gcp_project}")
73
+ end
74
+ FastlaneCore::UI.abort_with_message!(summarized_error)
75
+ return nil
76
+ else
77
+ response_json = JSON.parse(resp.body)
78
+ @default_bucket = response_json["defaultBucket"]
79
+ return @default_bucket
80
+ end
81
+ end
82
+
83
+ def start_job(gcp_project, app_path, result_path, devices, timeout_sec)
84
+ body = {
85
+ projectId: gcp_project,
86
+ testSpecification: {
87
+ testTimeout: {
88
+ seconds: timeout_sec
89
+ },
90
+ iosTestSetup: {},
91
+ iosXcTest: {
92
+ testsZip: {
93
+ gcsPath: app_path
94
+ }
95
+ }
96
+ },
97
+ environmentMatrix: {
98
+ iosDeviceList: {
99
+ iosDevices: devices.map(&FirebaseTestLabService.method(:map_device_to_proto))
100
+ }
101
+ },
102
+ resultStorage: {
103
+ googleCloudStorage: {
104
+ gcsPath: result_path
105
+ }
106
+ },
107
+ clientInfo: {
108
+ name: PLUGIN_NAME,
109
+ clientInfoDetails: [
110
+ {
111
+ key: "version",
112
+ value: VERSION
113
+ }
114
+ ]
115
+ }
116
+ }
117
+
118
+ conn = Faraday.new(FIREBASE_TEST_LAB_ENDPOINT)
119
+ begin
120
+ resp = conn.post(FTL_CREATE_API.gsub("{project}", gcp_project)) do |req|
121
+ req.headers = @auth.apply(req.headers)
122
+ req.headers["Content-Type"] = "application/json"
123
+ req.headers["X-Goog-User-Project"] = gcp_project
124
+ req.body = body.to_json
125
+ req.options.timeout = 15
126
+ req.options.open_timeout = 5
127
+ end
128
+ rescue Faraday::Error => ex
129
+ UI.abort_with_message!("Network error when initializing Firebase Test Lab, " \
130
+ "type: #{ex.class}, message: #{ex.message}")
131
+ end
132
+
133
+ if resp.status != 200
134
+ FastlaneCore::UI.error("Failed to start Firebase Test Lab jobs.")
135
+ FastlaneCore::UI.abort_with_message!(ErrorHelper.summarize_google_error(resp.body))
136
+ else
137
+ response_json = JSON.parse(resp.body)
138
+ return response_json["testMatrixId"]
139
+ end
140
+ end
141
+
142
+ def get_matrix_results(gcp_project, matrix_id)
143
+ url = FTL_RESULTS_API
144
+ .gsub("{project}", gcp_project)
145
+ .gsub("{matrix}", matrix_id)
146
+
147
+ conn = Faraday.new(FIREBASE_TEST_LAB_ENDPOINT)
148
+ begin
149
+ resp = conn.get(url) do |req|
150
+ req.headers = @auth.apply(req.headers)
151
+ req.options.timeout = 15
152
+ req.options.open_timeout = 5
153
+ end
154
+ rescue Faraday::Error => ex
155
+ UI.abort_with_message!("Network error when attempting to get test results, " \
156
+ "type: #{ex.class}, message: #{ex.message}")
157
+ end
158
+
159
+ if resp.status != 200
160
+ FastlaneCore::UI.error("Failed to obtain test results.")
161
+ FastlaneCore::UI.abort_with_message!(ErrorHelper.summarize_google_error(resp.body))
162
+ return nil
163
+ else
164
+ return JSON.parse(resp.body)
165
+ end
166
+ end
167
+
168
+ def get_execution_steps(gcp_project, history_id, execution_id)
169
+ conn = Faraday.new(APIARY_ENDPOINT)
170
+ url = TOOLRESULTS_LIST_EXECUTION_STEP_API_V3
171
+ .gsub("{project}", gcp_project)
172
+ .gsub("{history_id}", history_id)
173
+ .gsub("{execution_id}", execution_id)
174
+ begin
175
+ resp = conn.get(url) do |req|
176
+ req.headers = @auth.apply(req.headers)
177
+ req.options.timeout = 15
178
+ req.options.open_timeout = 5
179
+ end
180
+ rescue Faraday::Error => ex
181
+ UI.abort_with_message!("Failed to obtain the metadata of test artifacts, " \
182
+ "type: #{ex.class}, message: #{ex.message}")
183
+ end
184
+
185
+ if resp.status != 200
186
+ FastlaneCore::UI.error("Failed to obtain the metadata of test artifacts.")
187
+ FastlaneCore::UI.abort_with_message!(ErrorHelper.summarize_google_error(resp.body))
188
+ end
189
+ return JSON.parse(resp.body)
190
+ end
191
+
192
+ def self.map_device_to_proto(device)
193
+ {
194
+ iosModelId: device[:ios_model_id],
195
+ iosVersionId: device[:ios_version_id],
196
+ locale: device[:locale],
197
+ orientation: device[:orientation]
198
+ }
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,35 @@
1
+ require 'zip'
2
+ require 'plist'
3
+
4
+ module Fastlane
5
+ module FirebaseTestLab
6
+ class IosValidator
7
+ def self.validate_ios_app(file_path)
8
+ absolute_path = File.expand_path(file_path)
9
+ begin
10
+ Zip::File.open(absolute_path) do |zip_file|
11
+ xctestrun_files = zip_file.glob("*.xctestrun")
12
+ if xctestrun_files.size != 1
13
+ UI.user_error!("app verification failed: There must be only one .xctestrun files in the ZIP file.")
14
+ end
15
+
16
+ conf = Plist.parse_xml(xctestrun_files.first.get_input_stream)
17
+ unless conf.size == 1
18
+ UI.user_error!("The app bundle may contain only one scheme, #{conf.size} found")
19
+ end
20
+ _, scheme_conf = conf.first
21
+ unless scheme_conf["IsUITestBundle"]
22
+ UI.user_error!("The app bundle is not a UI test bundle. Did you build with build-for-testing argument?")
23
+ end
24
+ unless scheme_conf.key?("TestHostPath") || scheme_conf.key?("TestBundlePath")
25
+ UI.user_error!("Either TestHostPath or TestBundlePath must be in the app bundle. Please check your " \
26
+ "xcodebuild arguments")
27
+ end
28
+ end
29
+ rescue Zip::Error => e
30
+ UI.user_error!("Failed to read the ZIP file #{file_path}: #{e.message}")
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,23 @@
1
+ require 'googleauth'
2
+ require 'google/cloud/storage'
3
+
4
+ module Fastlane
5
+ module FirebaseTestLab
6
+ class Storage
7
+ GCS_OAUTH_SCOPES = ["https://www.googleapis.com/auth/devstorage.full_control"]
8
+
9
+ private_constant :GCS_OAUTH_SCOPES
10
+
11
+ def initialize(gcp_project, credential)
12
+ credentials = credential.get_google_credential(GCS_OAUTH_SCOPES)
13
+ @client = Google::Cloud::Storage.new(project_id: gcp_project,
14
+ credentials: credentials)
15
+ end
16
+
17
+ def upload_file(source_path, destination_bucket, destination_path)
18
+ bucket = @client.bucket(destination_bucket)
19
+ bucket.create_file(source_path, destination_path)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,6 @@
1
+ module Fastlane
2
+ module FirebaseTestLab
3
+ VERSION = "0.9.0"
4
+ PLUGIN_NAME = "fastlane-plugin-firebase_test_lab"
5
+ end
6
+ end
@@ -0,0 +1,90 @@
1
+ require 'fastlane_core/configuration/config_item'
2
+
3
+ module Fastlane
4
+ module FirebaseTestLab
5
+ class Options
6
+ def self.available_options
7
+ [
8
+ FastlaneCore::ConfigItem.new(key: :gcp_project,
9
+ description: "Google Cloud Platform project name",
10
+ optional: false),
11
+ FastlaneCore::ConfigItem.new(key: :app_path,
12
+ description: "Path to the app, either on the filesystem or GCS address (gs://)",
13
+ default_value:
14
+ Actions.lane_context[Actions::SharedValues::SCAN_ZIP_BUILD_PRODUCTS_PATH],
15
+ verify_block: proc do |value|
16
+ unless value.to_s.start_with?("gs://")
17
+ v = File.expand_path(value.to_s)
18
+ UI.user_error!("App file not found at path '#{v}'") unless File.exist?(v)
19
+ end
20
+ end),
21
+ FastlaneCore::ConfigItem.new(key: :devices,
22
+ description: "Devices to test the app on",
23
+ type: Array,
24
+ default_value: [{
25
+ ios_model_id: "iphonex",
26
+ ios_version_id: "11.2",
27
+ locale: "en_US",
28
+ orientation: "portrait"
29
+ }],
30
+ verify_block: proc do |value|
31
+ if value.empty?
32
+ UI.user_error!("Devices cannot be empty")
33
+ end
34
+ value.each do |current|
35
+ if current.class != Hash
36
+ UI.user_error!("Each device must be represented by a Hash object, " \
37
+ "#{current.class} found")
38
+ end
39
+ check_has_property(current, :ios_model_id)
40
+ check_has_property(current, :ios_version_id)
41
+ set_default_property(current, :locale, "en_US")
42
+ set_default_property(current, :orientation, "portrait")
43
+ end
44
+ end),
45
+ FastlaneCore::ConfigItem.new(key: :async,
46
+ description: "Do not wait for test results",
47
+ default_value: false,
48
+ type: Fastlane::Boolean),
49
+ FastlaneCore::ConfigItem.new(key: :timeout_sec,
50
+ description: "After how long, in seconds, should tests be terminated",
51
+ default_value: 180,
52
+ optional: true,
53
+ type: Integer,
54
+ verify_block: proc do |value|
55
+ UI.user_error!("Timeout must be less or equal to 45 minutes.") \
56
+ if value <= 0 || value > 45 * 60
57
+ end),
58
+ FastlaneCore::ConfigItem.new(key: :result_storage,
59
+ description: "GCS path to store test results",
60
+ default_value: nil,
61
+ optional: true,
62
+ verify_block: proc do |value|
63
+ UI.user_error!("Invalid GCS path: '#{value}'") \
64
+ unless value.to_s.start_with?("gs://")
65
+ end),
66
+ FastlaneCore::ConfigItem.new(key: :oauth_key_file_path,
67
+ description: "Use the given Google cloud service key file." \
68
+ "If not set, application default credential will be used " \
69
+ "(see https://cloud.google.com/docs/authentication/production)",
70
+ default_value: nil,
71
+ optional: true,
72
+ verify_block: proc do |value|
73
+ v = File.expand_path(value.to_s)
74
+ UI.user_error!("Key file not found at path '#{v}'") unless File.exist?(v)
75
+ end)
76
+ ]
77
+ end
78
+
79
+ def self.check_has_property(hash_obj, property)
80
+ UI.user_error!("Each device must have #{property} property") unless hash_obj.key?(property)
81
+ end
82
+
83
+ def self.set_default_property(hash_obj, property, default)
84
+ unless hash_obj.key?(property)
85
+ hash_obj[property] = default
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
metadata ADDED
@@ -0,0 +1,187 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fastlane-plugin-firebase_test_lab
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - Shihua Zheng
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-09-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: googleauth
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubyzip
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 1.0.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 1.0.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: plist
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.0.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 3.0.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: google-cloud-storage
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 1.13.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 1.13.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: tty-spinner
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 0.8.0
90
+ - - "<"
91
+ - !ruby/object:Gem::Version
92
+ version: 1.0.0
93
+ type: :runtime
94
+ prerelease: false
95
+ version_requirements: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: 0.8.0
100
+ - - "<"
101
+ - !ruby/object:Gem::Version
102
+ version: 1.0.0
103
+ - !ruby/object:Gem::Dependency
104
+ name: bundler
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ - !ruby/object:Gem::Dependency
118
+ name: fastlane
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: 2.102.0
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: 2.102.0
131
+ - !ruby/object:Gem::Dependency
132
+ name: rubocop
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "<="
136
+ - !ruby/object:Gem::Version
137
+ version: 0.50.0
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "<="
143
+ - !ruby/object:Gem::Version
144
+ version: 0.50.0
145
+ description:
146
+ email: shihuaz@google.com
147
+ executables: []
148
+ extensions: []
149
+ extra_rdoc_files: []
150
+ files:
151
+ - LICENSE
152
+ - README.md
153
+ - lib/fastlane/plugin/firebase_test_lab.rb
154
+ - lib/fastlane/plugin/firebase_test_lab/actions/firebase_test_lab_ios_xctest.rb
155
+ - lib/fastlane/plugin/firebase_test_lab/helper/credential.rb
156
+ - lib/fastlane/plugin/firebase_test_lab/helper/error_helper.rb
157
+ - lib/fastlane/plugin/firebase_test_lab/helper/ftl_message.rb
158
+ - lib/fastlane/plugin/firebase_test_lab/helper/ftl_service.rb
159
+ - lib/fastlane/plugin/firebase_test_lab/helper/ios_validator.rb
160
+ - lib/fastlane/plugin/firebase_test_lab/helper/storage.rb
161
+ - lib/fastlane/plugin/firebase_test_lab/module.rb
162
+ - lib/fastlane/plugin/firebase_test_lab/options.rb
163
+ homepage: https://github.com/fastlane/fastlane-plugin-firebase_test_lab
164
+ licenses:
165
+ - MIT
166
+ metadata: {}
167
+ post_install_message:
168
+ rdoc_options: []
169
+ require_paths:
170
+ - lib
171
+ required_ruby_version: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - ">="
174
+ - !ruby/object:Gem::Version
175
+ version: '0'
176
+ required_rubygems_version: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ requirements: []
182
+ rubyforge_project:
183
+ rubygems_version: 2.7.6
184
+ signing_key:
185
+ specification_version: 4
186
+ summary: Firebase Test Lab for fastlane
187
+ test_files: []