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.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +99 -0
- data/lib/fastlane/plugin/firebase_test_lab.rb +16 -0
- data/lib/fastlane/plugin/firebase_test_lab/actions/firebase_test_lab_ios_xctest.rb +268 -0
- data/lib/fastlane/plugin/firebase_test_lab/helper/credential.rb +33 -0
- data/lib/fastlane/plugin/firebase_test_lab/helper/error_helper.rb +21 -0
- data/lib/fastlane/plugin/firebase_test_lab/helper/ftl_message.rb +51 -0
- data/lib/fastlane/plugin/firebase_test_lab/helper/ftl_service.rb +202 -0
- data/lib/fastlane/plugin/firebase_test_lab/helper/ios_validator.rb +35 -0
- data/lib/fastlane/plugin/firebase_test_lab/helper/storage.rb +23 -0
- data/lib/fastlane/plugin/firebase_test_lab/module.rb +6 -0
- data/lib/fastlane/plugin/firebase_test_lab/options.rb +90 -0
- metadata +187 -0
checksums.yaml
ADDED
@@ -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
|
+
|
data/README.md
ADDED
@@ -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,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: []
|