fastlane-plugin-developer_device_platform 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +168 -0
- data/lib/fastlane/plugin/developer_device_platform/actions/developer_device_platform_action.rb +161 -0
- data/lib/fastlane/plugin/developer_device_platform/helper/device_run_client.rb +50 -0
- data/lib/fastlane/plugin/developer_device_platform/helper/gcloud.rb +61 -0
- data/lib/fastlane/plugin/developer_device_platform/helper/instrumentation_command.rb +90 -0
- data/lib/fastlane/plugin/developer_device_platform/helper/options.rb +122 -0
- data/lib/fastlane/plugin/developer_device_platform/helper/session.rb +111 -0
- data/lib/fastlane/plugin/developer_device_platform/helper/session_summary.rb +49 -0
- data/lib/fastlane/plugin/developer_device_platform/version.rb +5 -0
- data/lib/fastlane/plugin/developer_device_platform.rb +13 -0
- metadata +221 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a99f79e05b0a9a125921c472bd1edac41dc27c87dac082aeef067c99a84586d3
|
|
4
|
+
data.tar.gz: 7e1751408cf3527c2b1ab83d8f66680137904a0acee6d82b4946c37b9b4a9ac1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d8e4599ff167590dc18b132e314e79e7ad7f9d4333f94c86999655e440898c89ceac4494185d780cc5763ad44e37f589ba6a12a821f452a7eaed05a1235f12a8
|
|
7
|
+
data.tar.gz: dacea5f0bc9b4b5596fa33bb75b799a525a355d3636f704a9733d97abed96fb3e7494ca3c552b65ec6773ba074668d9572c06fc1ac9166081e9f153bc5ec7ea2
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ntsk
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# fastlane-plugin-developer_device_platform
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/fastlane-plugin-developer_device_platform)
|
|
4
|
+
[](https://github.com/ntsk/fastlane-plugin-developer_device_platform/actions/workflows/test.yml)
|
|
5
|
+
|
|
6
|
+
Run Android instrumentation tests on [Google Cloud Developer Device Platform](https://docs.cloud.google.com/developer-device-platform) (Device Run) from fastlane.
|
|
7
|
+
|
|
8
|
+
Firebase Test Lab is [deprecated and shuts down on September 30, 2027](https://firebase.google.com/docs/test-lab/migrate-faq). Developer Device Platform is its successor on Google Cloud. This plugin is a drop-in replacement for [fastlane-plugin-firebase_test_lab_android](https://github.com/cats-oss/fastlane-plugin-firebase_test_lab_android) built on `gcloud beta device-run`.
|
|
9
|
+
|
|
10
|
+
## What the action does
|
|
11
|
+
|
|
12
|
+
1. Uploads the app and test APKs to Cloud Storage and submits a Device Run session (one job per device).
|
|
13
|
+
2. Waits for the session to complete, streaming gcloud's progress to the fastlane log.
|
|
14
|
+
3. Reads the full session report, prints a per-job result table and the Cloud Console link.
|
|
15
|
+
4. Optionally downloads the artifacts (`junit.xml`, `logcat.txt`, `video.mp4`, ...) to a local directory.
|
|
16
|
+
5. Fails the lane unless the session result is `PASSED` (configurable).
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
- [Google Cloud CLI](https://cloud.google.com/sdk/docs/install) on the machine running fastlane (`gcloud components update` if it predates Device Run).
|
|
21
|
+
- A Google Cloud project with billing enabled and the APIs turned on:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
gcloud services enable devicerun.googleapis.com devicestreaming.googleapis.com testing.googleapis.com
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- The account running the tests needs `roles/devicerun.admin` and `roles/serviceusage.serviceUsageConsumer` on the project (see the [quickstart](https://docs.cloud.google.com/developer-device-platform/quickstart)).
|
|
28
|
+
- Authentication for gcloud. Either log in interactively (`gcloud auth login`) or pass a service account key with `service_account_key_file` (see [Authentication](#authentication)).
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
fastlane add_plugin developer_device_platform
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
lane :instrumentation_tests do
|
|
40
|
+
gradle(task: "assembleDebug assembleDebugAndroidTest")
|
|
41
|
+
|
|
42
|
+
developer_device_platform(
|
|
43
|
+
project_id: "my-gcp-project",
|
|
44
|
+
devices: ["shiba-34", "tokay-36"],
|
|
45
|
+
apps: ["app/build/outputs/apk/debug/app-debug.apk"],
|
|
46
|
+
test_apk: "app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk",
|
|
47
|
+
orchestrator_version: "auto",
|
|
48
|
+
instrumentation_timeout: "15m",
|
|
49
|
+
video: "on-failure",
|
|
50
|
+
download_dir: "build/device-run"
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Find device IDs with `gcloud beta device-run devices list` (for example `--filter="HARDWARE_TYPE=PHYSICAL AND OS_VERSION=35"`). An ID is `<model>-<api level>`, such as `shiba-34`, or a descriptive name for virtual devices, such as `mediumphone-arm-34`.
|
|
56
|
+
|
|
57
|
+
Inputs may also be `gs://` URIs, in which case nothing is uploaded:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
developer_device_platform(
|
|
61
|
+
devices: ["mediumphone-arm-34"],
|
|
62
|
+
apps: ["gs://my-bucket/app-debug.apk"],
|
|
63
|
+
test_apk: "gs://my-bucket/app-debug-androidTest.apk",
|
|
64
|
+
sharding_option: "smart",
|
|
65
|
+
smart_sharding_target_duration: "5m",
|
|
66
|
+
smart_sharding_record_name: "nightly",
|
|
67
|
+
flaky_test_attempts: 2,
|
|
68
|
+
flaky_test_retry_level: "test"
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Options
|
|
73
|
+
|
|
74
|
+
Options mirror the flags of `gcloud beta device-run sessions submit instrumentation`. Every option can also be set through the environment variable `DEVELOPER_DEVICE_PLATFORM_<OPTION>`.
|
|
75
|
+
|
|
76
|
+
| Option | Type | Description |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| `devices` | Array | **Required.** Device type IDs. One job is created per device. |
|
|
79
|
+
| `test_apk` | String | **Required.** Instrumentation test APK, local path or `gs://` URI. |
|
|
80
|
+
| `apps` | Array | App files (`.apk`, `.aab`, `.apks`) installed before the test, in order. |
|
|
81
|
+
| `project_id` | String | Google Cloud project. Defaults to the active gcloud configuration. |
|
|
82
|
+
| `service_account_key_file` | String | Service account key JSON used to authenticate gcloud for this action only. |
|
|
83
|
+
| `gcloud_binary` | String | Path to `gcloud`. Default `gcloud`. |
|
|
84
|
+
| `location` | String | Cloud location of the session. Default `global`. |
|
|
85
|
+
| `bucket_name` | String | Bucket for inputs and results. Defaults to `PROJECT_ID-devicerun`, created on first use. |
|
|
86
|
+
| `test_runner_class` | String | Instrumentation runner class. Defaults to the manifest value. |
|
|
87
|
+
| `test_targets` | Array | Targets such as `"class com.example.FooTest"` or `"notPackage com.example.slow"`. |
|
|
88
|
+
| `additional_test_options` | Hash | Options passed to the runner, e.g. `{ clearPackageData: "true" }`. |
|
|
89
|
+
| `other_files_to_push` | Hash | Files pushed before the test, `{ "local/or/gs/path" => "/sdcard/dest" }`. |
|
|
90
|
+
| `paths_to_pull` | Array | Device paths pulled after the test. |
|
|
91
|
+
| `instrumentation_timeout` | String | `1m` to `3h`. Default `5m`. |
|
|
92
|
+
| `orchestrator_version` | String | Android Test Orchestrator version or `auto`. Unset means no orchestrator. |
|
|
93
|
+
| `labels` | Hash | Labels attached to the session. |
|
|
94
|
+
| `locale` | String | Device locale, e.g. `ja-JP`. |
|
|
95
|
+
| `orientation` | String | `portrait` or `landscape`. |
|
|
96
|
+
| `coordinates` | String | Mock GPS location `latitude,longitude`. |
|
|
97
|
+
| `video` | String | `always` or `on-failure`. |
|
|
98
|
+
| `dumpsys` | String | `always` or `on-failure`. |
|
|
99
|
+
| `bugreport` | String | `always` or `on-failure`. |
|
|
100
|
+
| `sharding_option` | String | `uniform` or `smart`. |
|
|
101
|
+
| `uniform_sharding_count` | Integer | Shard count. Required with `uniform`. |
|
|
102
|
+
| `smart_sharding_target_duration` | String | Target duration per shard, `2m` to `1h`. Required with `smart`. |
|
|
103
|
+
| `smart_sharding_record_name` | String | Timing record name without extension. Required with `smart`. |
|
|
104
|
+
| `smart_sharding_max_shard_count` | Integer | Maximum smart shards. `0` uses the system limit. |
|
|
105
|
+
| `flaky_test_attempts` | Integer | Attempts per shard. Default `1`. |
|
|
106
|
+
| `flaky_test_parallel_retry` | Boolean | Retry in parallel instead of sequentially. |
|
|
107
|
+
| `flaky_test_retry_level` | String | `shard` or `test`. |
|
|
108
|
+
| `async` | Boolean | Return the session ID right after submission. Default `false`. |
|
|
109
|
+
| `download_dir` | String | Local directory to download the session artifacts into. |
|
|
110
|
+
| `fail_on_failure` | Boolean | Fail the lane when the result is not `PASSED`. Default `true`. |
|
|
111
|
+
|
|
112
|
+
### Return value and lane context
|
|
113
|
+
|
|
114
|
+
The action returns a `Fastlane::DeveloperDevicePlatform::Session` (or the session ID when `async` is true) and sets:
|
|
115
|
+
|
|
116
|
+
| Lane context key | Value |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| `DEVELOPER_DEVICE_PLATFORM_SESSION_ID` | Session ID |
|
|
119
|
+
| `DEVELOPER_DEVICE_PLATFORM_RESULT` | `PASSED`, `FAILED`, `ERROR`, `TIMED_OUT` or `CANCELLED` |
|
|
120
|
+
| `DEVELOPER_DEVICE_PLATFORM_OUTPUT_DIRECTORY` | `gs://BUCKET/automation/sessions/SESSION_ID` |
|
|
121
|
+
| `DEVELOPER_DEVICE_PLATFORM_CONSOLE_URL` | Cloud Console link to the artifacts |
|
|
122
|
+
| `DEVELOPER_DEVICE_PLATFORM_DOWNLOAD_DIR` | Local artifact directory when `download_dir` is set |
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
session = developer_device_platform(devices: ["shiba-34"], test_apk: "test.apk", fail_on_failure: false)
|
|
126
|
+
session.jobs.each { |job| puts "#{job.display_name}: #{job.result} #{job.message}" }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Results
|
|
130
|
+
|
|
131
|
+
Artifacts land in `gs://BUCKET/automation/sessions/SESSION_ID/job-NNN/`: `merged_junit.xml` per job, and `junit.xml`, `instrumentation.log`, `logcat.txt`, `video.mp4` and pulled files per execution. With `download_dir` the same tree is copied to `<download_dir>/<SESSION_ID>/`, so JUnit reporters can pick up `**/merged_junit.xml`.
|
|
132
|
+
|
|
133
|
+
### Authentication
|
|
134
|
+
|
|
135
|
+
On CI, the simplest setup is a service account key passed through `service_account_key_file`. The plugin hands it to gcloud via `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE`, so the global gcloud configuration is left untouched. If your CI already authenticates gcloud (for example with Workload Identity Federation through `google-github-actions/auth`), omit the option and the active credentials are used.
|
|
136
|
+
|
|
137
|
+
Device Run reads inputs and writes results through its own service agent (`service-PROJECT_NUMBER@gcp-sa-devicerun.iam.gserviceaccount.com`). When you point `bucket_name` at a bucket in another project, grant that agent `roles/storage.objectViewer` on it (see [Share storage](https://docs.cloud.google.com/developer-device-platform/device-run/share-storage)).
|
|
138
|
+
|
|
139
|
+
## Migrating from fastlane-plugin-firebase_test_lab_android
|
|
140
|
+
|
|
141
|
+
| firebase_test_lab_android | developer_device_platform |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| `project_id` | `project_id` |
|
|
144
|
+
| `gcloud_service_key_file` | `service_account_key_file` |
|
|
145
|
+
| `type: "instrumentation"` | Only instrumentation tests are supported by Device Run. Robo tests have no equivalent. |
|
|
146
|
+
| `devices: [{ model:, version:, locale:, orientation: }]` | `devices: ["<model>-<version>"]`, plus `locale` and `orientation` applied to every device |
|
|
147
|
+
| `app_apk` / `app_test_apk` | `apps: [app_apk]` / `test_apk` |
|
|
148
|
+
| `use_orchestrator: true` | `orchestrator_version: "auto"` |
|
|
149
|
+
| `timeout` | `instrumentation_timeout` |
|
|
150
|
+
| `firebase_test_lab_results_bucket` | `bucket_name` |
|
|
151
|
+
| `firebase_test_lab_results_dir` | Not supported. Results are grouped by session ID. |
|
|
152
|
+
| `download_dir` | `download_dir` |
|
|
153
|
+
| `extra_options` | The corresponding typed option (`test_targets`, `additional_test_options`, `video`, ...) |
|
|
154
|
+
| `slack_url`, `github_*` | Not included. Use `slack` or GitHub actions with the lane context values above. |
|
|
155
|
+
|
|
156
|
+
## Development
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
bundle install
|
|
160
|
+
bundle exec rake # rspec + rubocop
|
|
161
|
+
bundle exec fastlane action developer_device_platform
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The gcloud integration is exercised against a fake `gcloud` executable in `spec/fixtures`, so the test suite needs neither credentials nor network access.
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT
|
data/lib/fastlane/plugin/developer_device_platform/actions/developer_device_platform_action.rb
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
require 'fastlane/action'
|
|
2
|
+
require_relative '../helper/device_run_client'
|
|
3
|
+
require_relative '../helper/gcloud'
|
|
4
|
+
require_relative '../helper/instrumentation_command'
|
|
5
|
+
require_relative '../helper/options'
|
|
6
|
+
require_relative '../helper/session_summary'
|
|
7
|
+
|
|
8
|
+
module Fastlane
|
|
9
|
+
module Actions
|
|
10
|
+
module SharedValues
|
|
11
|
+
DEVELOPER_DEVICE_PLATFORM_SESSION_ID = :DEVELOPER_DEVICE_PLATFORM_SESSION_ID
|
|
12
|
+
DEVELOPER_DEVICE_PLATFORM_RESULT = :DEVELOPER_DEVICE_PLATFORM_RESULT
|
|
13
|
+
DEVELOPER_DEVICE_PLATFORM_OUTPUT_DIRECTORY = :DEVELOPER_DEVICE_PLATFORM_OUTPUT_DIRECTORY
|
|
14
|
+
DEVELOPER_DEVICE_PLATFORM_CONSOLE_URL = :DEVELOPER_DEVICE_PLATFORM_CONSOLE_URL
|
|
15
|
+
DEVELOPER_DEVICE_PLATFORM_DOWNLOAD_DIR = :DEVELOPER_DEVICE_PLATFORM_DOWNLOAD_DIR
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class DeveloperDevicePlatformAction < Action
|
|
19
|
+
COMMAND_OPTIONS = DeveloperDevicePlatform::InstrumentationCommand::FLAGS.keys
|
|
20
|
+
RECOVERABLE_ERRORS = [
|
|
21
|
+
DeveloperDevicePlatform::Gcloud::CommandError,
|
|
22
|
+
DeveloperDevicePlatform::DeviceRunClient::Error,
|
|
23
|
+
DeveloperDevicePlatform::Session::ParseError
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
def self.run(params)
|
|
27
|
+
client = DeveloperDevicePlatform::DeviceRunClient.new(gcloud: build_gcloud(params))
|
|
28
|
+
location = params[:location]
|
|
29
|
+
|
|
30
|
+
UI.message("Submitting instrumentation session to Developer Device Platform...")
|
|
31
|
+
session_id = client.submit(build_command(params)) { |line| UI.message(line) }
|
|
32
|
+
lane_context[SharedValues::DEVELOPER_DEVICE_PLATFORM_SESSION_ID] = session_id
|
|
33
|
+
UI.success("Created Device Run session #{session_id}")
|
|
34
|
+
return session_id if params[:async]
|
|
35
|
+
|
|
36
|
+
UI.message("Waiting for session #{session_id} to complete...")
|
|
37
|
+
client.wait(session_id, location: location) { |line| UI.message(line) }
|
|
38
|
+
session = client.describe(session_id, location: location)
|
|
39
|
+
publish(session)
|
|
40
|
+
download(client, session, params[:download_dir])
|
|
41
|
+
UI.message(DeveloperDevicePlatform::SessionSummary.new(session).to_s)
|
|
42
|
+
conclude(session, fail_on_failure: params[:fail_on_failure])
|
|
43
|
+
rescue *RECOVERABLE_ERRORS => e
|
|
44
|
+
UI.user_error!(e.message)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.build_gcloud(params)
|
|
48
|
+
DeveloperDevicePlatform::Gcloud.new(
|
|
49
|
+
binary: params[:gcloud_binary],
|
|
50
|
+
project: params[:project_id],
|
|
51
|
+
credential_file: params[:service_account_key_file]
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.build_command(params)
|
|
56
|
+
DeveloperDevicePlatform::InstrumentationCommand.new(**COMMAND_OPTIONS.to_h { |key| [key, params[key]] })
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.publish(session)
|
|
60
|
+
lane_context[SharedValues::DEVELOPER_DEVICE_PLATFORM_RESULT] = session.result
|
|
61
|
+
lane_context[SharedValues::DEVELOPER_DEVICE_PLATFORM_OUTPUT_DIRECTORY] = session.output_directory
|
|
62
|
+
lane_context[SharedValues::DEVELOPER_DEVICE_PLATFORM_CONSOLE_URL] = session.console_url
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.download(client, session, destination)
|
|
66
|
+
return if destination.nil?
|
|
67
|
+
|
|
68
|
+
UI.message("Downloading results of session #{session.id} to #{destination}...")
|
|
69
|
+
local_path = client.download(session, destination) { |line| UI.verbose(line) }
|
|
70
|
+
lane_context[SharedValues::DEVELOPER_DEVICE_PLATFORM_DOWNLOAD_DIR] = local_path
|
|
71
|
+
UI.success("Results downloaded to #{local_path}")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.conclude(session, fail_on_failure:)
|
|
75
|
+
outcome = "Device Run session #{session.id} finished with result #{session.result}"
|
|
76
|
+
if session.passed?
|
|
77
|
+
UI.success(outcome)
|
|
78
|
+
elsif fail_on_failure
|
|
79
|
+
UI.test_failure!(outcome)
|
|
80
|
+
else
|
|
81
|
+
UI.important(outcome)
|
|
82
|
+
end
|
|
83
|
+
session
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.description
|
|
87
|
+
"Run Android instrumentation tests on Google Cloud Developer Device Platform (Device Run)"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.details
|
|
91
|
+
[
|
|
92
|
+
"Uploads your app and test APKs to Google Cloud Storage, submits an instrumentation session to Device Run",
|
|
93
|
+
"(the successor of Firebase Test Lab), waits for it to finish and reports the per-job results.",
|
|
94
|
+
"Requires the Google Cloud CLI (`gcloud`) with the Device Run API (devicerun.googleapis.com) enabled on the project.",
|
|
95
|
+
"Options mirror the flags of `gcloud beta device-run sessions submit instrumentation`."
|
|
96
|
+
].join(" ")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.authors
|
|
100
|
+
["ntsk"]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.return_value
|
|
104
|
+
"The DeveloperDevicePlatform::Session describing the finished session, or the session id when `async` is true"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self.output
|
|
108
|
+
[
|
|
109
|
+
["DEVELOPER_DEVICE_PLATFORM_SESSION_ID", "ID of the created Device Run session"],
|
|
110
|
+
["DEVELOPER_DEVICE_PLATFORM_RESULT", "Overall result (PASSED, FAILED, ERROR, TIMED_OUT, CANCELLED)"],
|
|
111
|
+
["DEVELOPER_DEVICE_PLATFORM_OUTPUT_DIRECTORY", "Cloud Storage directory (gs://...) holding the session artifacts"],
|
|
112
|
+
["DEVELOPER_DEVICE_PLATFORM_CONSOLE_URL", "Cloud Console URL of the session artifacts"],
|
|
113
|
+
["DEVELOPER_DEVICE_PLATFORM_DOWNLOAD_DIR", "Local directory the artifacts were downloaded to, when download_dir is set"]
|
|
114
|
+
]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.example_code
|
|
118
|
+
[
|
|
119
|
+
<<~EXAMPLE,
|
|
120
|
+
developer_device_platform(
|
|
121
|
+
project_id: "my-gcp-project",
|
|
122
|
+
devices: ["shiba-34", "tokay-36"],
|
|
123
|
+
apps: ["app/build/outputs/apk/debug/app-debug.apk"],
|
|
124
|
+
test_apk: "app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk",
|
|
125
|
+
orchestrator_version: "auto",
|
|
126
|
+
instrumentation_timeout: "15m",
|
|
127
|
+
video: "on-failure",
|
|
128
|
+
download_dir: "build/device-run"
|
|
129
|
+
)
|
|
130
|
+
EXAMPLE
|
|
131
|
+
<<~EXAMPLE
|
|
132
|
+
developer_device_platform(
|
|
133
|
+
devices: ["mediumphone-arm-34"],
|
|
134
|
+
test_apk: "gs://my-bucket/app-debug-androidTest.apk",
|
|
135
|
+
apps: ["gs://my-bucket/app-debug.apk"],
|
|
136
|
+
sharding_option: "smart",
|
|
137
|
+
smart_sharding_target_duration: "5m",
|
|
138
|
+
smart_sharding_record_name: "nightly",
|
|
139
|
+
flaky_test_attempts: 2,
|
|
140
|
+
flaky_test_retry_level: "test"
|
|
141
|
+
)
|
|
142
|
+
EXAMPLE
|
|
143
|
+
]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def self.category
|
|
147
|
+
:testing
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.is_supported?(platform)
|
|
151
|
+
platform == :android
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def self.available_options
|
|
155
|
+
DeveloperDevicePlatform::Options.all
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private_class_method :build_gcloud, :build_command, :publish, :download, :conclude
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require_relative 'gcloud'
|
|
3
|
+
require_relative 'session'
|
|
4
|
+
|
|
5
|
+
module Fastlane
|
|
6
|
+
module DeveloperDevicePlatform
|
|
7
|
+
class DeviceRunClient
|
|
8
|
+
class Error < StandardError; end
|
|
9
|
+
|
|
10
|
+
SESSION_ANNOUNCEMENT = /Creating session \[([^\]]+)\]/
|
|
11
|
+
|
|
12
|
+
attr_reader :gcloud
|
|
13
|
+
|
|
14
|
+
def initialize(gcloud:)
|
|
15
|
+
@gcloud = gcloud
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def submit(command, &progress)
|
|
19
|
+
output = device_run("sessions", "submit", "instrumentation", *command.arguments, "--async", &progress)
|
|
20
|
+
output.stderr[SESSION_ANNOUNCEMENT, 1] ||
|
|
21
|
+
raise(Error, "Could not find the session id in gcloud output:\n#{output.stderr}")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def wait(session_id, location:, &progress)
|
|
25
|
+
device_run("sessions", "wait", session_id, "--location=#{location}", &progress)
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def describe(session_id, location:)
|
|
30
|
+
output = device_run("sessions", "describe", session_id, "--location=#{location}", "--full", "--format=json")
|
|
31
|
+
Session.parse(output.stdout)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def download(session, destination, &progress)
|
|
35
|
+
source = session.output_directory
|
|
36
|
+
raise Error, "Session #{session.id} has no output directory to download" if source.nil?
|
|
37
|
+
|
|
38
|
+
FileUtils.mkdir_p(destination)
|
|
39
|
+
gcloud.run("storage", "cp", "--recursive", source, destination, &progress)
|
|
40
|
+
File.join(destination, session.id)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def device_run(*args, &progress)
|
|
46
|
+
gcloud.run("beta", "device-run", *args, &progress)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require 'open3'
|
|
2
|
+
|
|
3
|
+
module Fastlane
|
|
4
|
+
module DeveloperDevicePlatform
|
|
5
|
+
class Gcloud
|
|
6
|
+
Output = Struct.new(:stdout, :stderr, keyword_init: true)
|
|
7
|
+
|
|
8
|
+
class CommandError < StandardError
|
|
9
|
+
attr_reader :command, :stderr, :exit_status
|
|
10
|
+
|
|
11
|
+
def initialize(command, stderr, exit_status)
|
|
12
|
+
@command = command
|
|
13
|
+
@stderr = stderr
|
|
14
|
+
@exit_status = exit_status
|
|
15
|
+
super("gcloud command failed#{" (exit #{exit_status})" if exit_status}: #{command.join(' ')}\n#{stderr}".rstrip)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_reader :binary, :project, :credential_file
|
|
20
|
+
|
|
21
|
+
def initialize(binary: "gcloud", project: nil, credential_file: nil)
|
|
22
|
+
@binary = binary
|
|
23
|
+
@project = project
|
|
24
|
+
@credential_file = credential_file
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run(*args, &on_stderr_line)
|
|
28
|
+
command = [binary, *args]
|
|
29
|
+
stdout, stderr, status = execute(command, &on_stderr_line)
|
|
30
|
+
raise CommandError.new(command, stderr, status.exitstatus) unless status.success?
|
|
31
|
+
|
|
32
|
+
Output.new(stdout:, stderr:)
|
|
33
|
+
rescue Errno::ENOENT => e
|
|
34
|
+
raise CommandError.new(command, e.message, nil)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def environment
|
|
38
|
+
{
|
|
39
|
+
"CLOUDSDK_CORE_DISABLE_PROMPTS" => "1",
|
|
40
|
+
"CLOUDSDK_CORE_PROJECT" => project,
|
|
41
|
+
"CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE" => credential_file
|
|
42
|
+
}.compact
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def execute(command)
|
|
48
|
+
Open3.popen3(environment, *command) do |stdin, stdout, stderr, waiter|
|
|
49
|
+
stdin.close
|
|
50
|
+
stdout_reader = Thread.new { stdout.read }
|
|
51
|
+
stderr_text = +""
|
|
52
|
+
stderr.each_line do |line|
|
|
53
|
+
stderr_text << line
|
|
54
|
+
yield(line.chomp) if block_given?
|
|
55
|
+
end
|
|
56
|
+
[stdout_reader.value, stderr_text, waiter.value]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module Fastlane
|
|
2
|
+
module DeveloperDevicePlatform
|
|
3
|
+
class InstrumentationCommand
|
|
4
|
+
module Flags
|
|
5
|
+
ALTERNATE_DELIMITER = "|".freeze
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def value(flag, value)
|
|
10
|
+
value.nil? ? [] : ["--#{flag}=#{value}"]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def list(flag, values)
|
|
14
|
+
values = Array(values).compact
|
|
15
|
+
values.empty? ? [] : ["--#{flag}=#{join(values)}"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def repeated(flag, values)
|
|
19
|
+
Array(values).compact.map { |item| "--#{flag}=#{join([item])}" }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def dict(flag, pairs)
|
|
23
|
+
(pairs || {}).map { |key, value| "--#{flag}=#{join(["#{key}=#{value}"])}" }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def boolean(flag, value)
|
|
27
|
+
case value
|
|
28
|
+
when true then ["--#{flag}"]
|
|
29
|
+
when false then ["--no-#{flag}"]
|
|
30
|
+
else []
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def join(items)
|
|
35
|
+
items = items.map(&:to_s)
|
|
36
|
+
return items.join(",") if items.none? { |item| item.include?(",") }
|
|
37
|
+
|
|
38
|
+
"^#{ALTERNATE_DELIMITER}^#{items.join(ALTERNATE_DELIMITER)}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
FLAGS = {
|
|
43
|
+
devices: ["device", :list],
|
|
44
|
+
test_apk: ["test", :value],
|
|
45
|
+
apps: ["apps", :list],
|
|
46
|
+
location: ["location", :value],
|
|
47
|
+
bucket_name: ["bucket-name", :value],
|
|
48
|
+
test_runner_class: ["test-runner-class", :value],
|
|
49
|
+
test_targets: ["test-targets", :repeated],
|
|
50
|
+
additional_test_options: ["additional-test-options", :dict],
|
|
51
|
+
other_files_to_push: ["other-files-to-push", :dict],
|
|
52
|
+
paths_to_pull: ["paths-to-pull", :list],
|
|
53
|
+
instrumentation_timeout: ["instrumentation-timeout", :value],
|
|
54
|
+
orchestrator_version: ["orchestrator-version", :value],
|
|
55
|
+
locale: ["locale", :value],
|
|
56
|
+
orientation: ["orientation", :value],
|
|
57
|
+
coordinates: ["coordinates", :value],
|
|
58
|
+
video: ["video", :value],
|
|
59
|
+
dumpsys: ["dumpsys", :value],
|
|
60
|
+
bugreport: ["bugreport", :value],
|
|
61
|
+
sharding_option: ["sharding-option", :value],
|
|
62
|
+
uniform_sharding_count: ["uniform-sharding-count", :value],
|
|
63
|
+
smart_sharding_target_duration: ["smart-sharding-target-duration", :value],
|
|
64
|
+
smart_sharding_record_name: ["smart-sharding-record-name", :value],
|
|
65
|
+
smart_sharding_max_shard_count: ["smart-sharding-max-shard-count", :value],
|
|
66
|
+
flaky_test_attempts: ["flaky-test-attempts", :value],
|
|
67
|
+
flaky_test_parallel_retry: ["flaky-test-parallel-retry", :boolean],
|
|
68
|
+
flaky_test_retry_level: ["flaky-test-retry-level", :value],
|
|
69
|
+
labels: ["labels", :dict]
|
|
70
|
+
}.freeze
|
|
71
|
+
|
|
72
|
+
attr_reader :options
|
|
73
|
+
|
|
74
|
+
def initialize(devices:, test_apk:, **options)
|
|
75
|
+
unknown = options.keys - FLAGS.keys
|
|
76
|
+
raise ArgumentError, "unknown options: #{unknown.join(', ')}" unless unknown.empty?
|
|
77
|
+
raise ArgumentError, "devices must contain at least one device id" if Array(devices).compact.empty?
|
|
78
|
+
raise ArgumentError, "test_apk is required" if test_apk.to_s.empty?
|
|
79
|
+
|
|
80
|
+
@options = { devices: Array(devices), test_apk: test_apk, **options }.freeze
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def arguments
|
|
84
|
+
FLAGS.flat_map do |key, (flag, kind)|
|
|
85
|
+
Flags.public_send(kind, flag, options[key])
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
require 'fastlane/boolean'
|
|
2
|
+
require 'fastlane_core/configuration/config_item'
|
|
3
|
+
require 'fastlane_core/ui/ui'
|
|
4
|
+
|
|
5
|
+
module Fastlane
|
|
6
|
+
UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
|
|
7
|
+
|
|
8
|
+
module DeveloperDevicePlatform
|
|
9
|
+
module Options
|
|
10
|
+
COLLECTION_MODES = ["always", "on-failure"].freeze
|
|
11
|
+
ORIENTATIONS = ["portrait", "landscape"].freeze
|
|
12
|
+
SHARDING_OPTIONS = ["uniform", "smart"].freeze
|
|
13
|
+
RETRY_LEVELS = ["shard", "test"].freeze
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def all
|
|
18
|
+
execution_options + device_options + sharding_options + retry_options + behaviour_options
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def execution_options
|
|
22
|
+
[
|
|
23
|
+
option(:gcloud_binary, "Path to the gcloud executable", default_value: "gcloud"),
|
|
24
|
+
option(:project_id, "Google Cloud project ID (defaults to the active gcloud configuration)"),
|
|
25
|
+
option(:service_account_key_file, "Path to a service account key JSON used to authenticate gcloud",
|
|
26
|
+
verify_block: file_exists("service_account_key_file")),
|
|
27
|
+
option(:location, "Cloud location of the session", default_value: "global"),
|
|
28
|
+
option(:devices, "Device type IDs to run on, e.g. [\"shiba-34\"]. One job is created per device",
|
|
29
|
+
type: Array, optional: false, verify_block: non_empty("devices")),
|
|
30
|
+
option(:test_apk, "Path to the instrumentation test APK (local or gs://)",
|
|
31
|
+
optional: false, verify_block: input_exists("test_apk")),
|
|
32
|
+
option(:apps, "Application files (.apk, .aab, .apks) to install before the test, in order (local or gs://)",
|
|
33
|
+
type: Array, verify_block: inputs_exist("apps")),
|
|
34
|
+
option(:bucket_name, "Cloud Storage bucket for inputs and results (defaults to PROJECT_ID-devicerun)"),
|
|
35
|
+
option(:test_runner_class, "Instrumentation test runner class (defaults to the one in the manifest)"),
|
|
36
|
+
option(:test_targets, "Test targets, e.g. [\"class com.example.FooTest\", \"notPackage com.example.slow\"]", type: Array),
|
|
37
|
+
option(:additional_test_options, "Key-value options passed to the instrumentation runner", type: Hash),
|
|
38
|
+
option(:other_files_to_push, "Files to push to the device before the test, as { source => destination }", type: Hash),
|
|
39
|
+
option(:paths_to_pull, "Device paths to pull after the test", type: Array),
|
|
40
|
+
option(:instrumentation_timeout, "Maximum duration of the test run, 1m to 3h (default 5m)"),
|
|
41
|
+
option(:orchestrator_version, "Android Test Orchestrator version, or 'auto' (unset means no orchestrator)"),
|
|
42
|
+
option(:labels, "Key-value labels attached to the session", type: Hash)
|
|
43
|
+
]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def device_options
|
|
47
|
+
[
|
|
48
|
+
option(:locale, "Device locale in language-region form, e.g. ja-JP"),
|
|
49
|
+
enum_option(:orientation, "Device orientation", ORIENTATIONS),
|
|
50
|
+
option(:coordinates, "Mock GPS location as latitude,longitude"),
|
|
51
|
+
enum_option(:video, "When to record a video of the screen", COLLECTION_MODES),
|
|
52
|
+
enum_option(:dumpsys, "When to collect dumpsys output", COLLECTION_MODES),
|
|
53
|
+
enum_option(:bugreport, "When to collect a bugreport", COLLECTION_MODES)
|
|
54
|
+
]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def sharding_options
|
|
58
|
+
[
|
|
59
|
+
enum_option(:sharding_option, "Sharding strategy", SHARDING_OPTIONS),
|
|
60
|
+
option(:uniform_sharding_count, "Number of uniform shards (required with sharding_option 'uniform')", type: Integer),
|
|
61
|
+
option(:smart_sharding_target_duration, "Target duration per shard, 2m to 1h (required with sharding_option 'smart')"),
|
|
62
|
+
option(:smart_sharding_record_name, "Timing record name without extension (required with sharding_option 'smart')"),
|
|
63
|
+
option(:smart_sharding_max_shard_count, "Maximum number of smart shards (0 uses the system limit)", type: Integer)
|
|
64
|
+
]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def retry_options
|
|
68
|
+
[
|
|
69
|
+
option(:flaky_test_attempts, "Maximum execution attempts per shard (default 1)", type: Integer),
|
|
70
|
+
option(:flaky_test_parallel_retry, "Retry failures in parallel instead of sequentially", type: Boolean),
|
|
71
|
+
enum_option(:flaky_test_retry_level, "Retry at the shard or the test level", RETRY_LEVELS)
|
|
72
|
+
]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def behaviour_options
|
|
76
|
+
[
|
|
77
|
+
option(:async, "Return the session id right after submission instead of waiting", type: Boolean, default_value: false),
|
|
78
|
+
option(:download_dir, "Local directory to download the session artifacts into"),
|
|
79
|
+
option(:fail_on_failure, "Fail the lane when the session result is not PASSED", type: Boolean, default_value: true)
|
|
80
|
+
]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def option(key, description, type: String, optional: true, default_value: nil, verify_block: nil)
|
|
84
|
+
FastlaneCore::ConfigItem.new(
|
|
85
|
+
key: key,
|
|
86
|
+
env_name: "DEVELOPER_DEVICE_PLATFORM_#{key.to_s.upcase}",
|
|
87
|
+
description: description,
|
|
88
|
+
type: type,
|
|
89
|
+
optional: optional,
|
|
90
|
+
default_value: default_value,
|
|
91
|
+
verify_block: verify_block
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def enum_option(key, description, values)
|
|
96
|
+
option(key, "#{description} (#{values.join(' | ')})", verify_block: proc do |value|
|
|
97
|
+
UI.user_error!("#{key} must be one of #{values.join(', ')}, got '#{value}'") unless values.include?(value)
|
|
98
|
+
end)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def non_empty(name)
|
|
102
|
+
proc { |value| UI.user_error!("#{name} must contain at least one entry") if value.empty? }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def file_exists(name)
|
|
106
|
+
proc { |path| UI.user_error!("Could not find #{name} at path '#{path}'") unless File.exist?(path) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def input_exists(name)
|
|
110
|
+
proc { |path| UI.user_error!("Could not find #{name} at path '#{path}'") unless remote?(path) || File.exist?(path) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def inputs_exist(name)
|
|
114
|
+
proc { |paths| paths.each { |path| input_exists(name).call(path) } }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def remote?(path)
|
|
118
|
+
path.start_with?("gs://")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
module Fastlane
|
|
4
|
+
module DeveloperDevicePlatform
|
|
5
|
+
class Session
|
|
6
|
+
class ParseError < StandardError; end
|
|
7
|
+
|
|
8
|
+
CONSOLE_STORAGE_BROWSER = "https://console.cloud.google.com/storage/browser/".freeze
|
|
9
|
+
|
|
10
|
+
module Outcome
|
|
11
|
+
def done?
|
|
12
|
+
status == "DONE"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def passed?
|
|
16
|
+
result == "PASSED"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
Execution = Struct.new(:display_name, :status, :result, :message, :output_files, keyword_init: true) do
|
|
21
|
+
include Outcome
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Job = Struct.new(:display_name, :status, :result, :message, :output_files, :executions, keyword_init: true) do
|
|
25
|
+
include Outcome
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.parse(json)
|
|
29
|
+
new(JSON.parse(json))
|
|
30
|
+
rescue JSON::ParserError => e
|
|
31
|
+
raise ParseError, "Could not parse session JSON: #{e.message}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(attributes)
|
|
35
|
+
@attributes = attributes
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def name
|
|
39
|
+
@attributes["name"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def id
|
|
43
|
+
name&.split("/")&.last || report["id"]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def status
|
|
47
|
+
report.dig("status", "statusType")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def done?
|
|
51
|
+
status == "DONE"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def result
|
|
55
|
+
report.dig("result", "resultType")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def passed?
|
|
59
|
+
result == "PASSED"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def result_message
|
|
63
|
+
report.dig("result", "cause", "summary", "message")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def output_directory
|
|
67
|
+
base = @attributes.dig("sessionConfig", "outputDirectoryConfig", "gcsOutputDirectory", "path")
|
|
68
|
+
return nil if base.nil? || id.nil?
|
|
69
|
+
|
|
70
|
+
"#{base.chomp('/')}/#{id}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def console_url
|
|
74
|
+
directory = output_directory
|
|
75
|
+
return nil if directory.nil?
|
|
76
|
+
|
|
77
|
+
"#{CONSOLE_STORAGE_BROWSER}#{directory.delete_prefix('gs://')}/"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def jobs
|
|
81
|
+
Array(report["jobReports"]).map do |job|
|
|
82
|
+
Job.new(**report_fields(job), executions: executions_of(job))
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def output_files
|
|
87
|
+
jobs.flat_map { |job| job.output_files + job.executions.flat_map(&:output_files) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def report
|
|
93
|
+
@attributes["sessionReport"] || {}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def executions_of(job)
|
|
97
|
+
Array(job["executionReports"]).map { |execution| Execution.new(**report_fields(execution)) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def report_fields(node)
|
|
101
|
+
{
|
|
102
|
+
display_name: node["displayName"],
|
|
103
|
+
status: node.dig("status", "statusType"),
|
|
104
|
+
result: node.dig("result", "resultType"),
|
|
105
|
+
message: node.dig("result", "cause", "summary", "message") || Array(node.dig("status", "progressMessages")).last,
|
|
106
|
+
output_files: Array(node["outputFiles"]).filter_map { |file| file.dig("gcsOutputFile", "path") }
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require 'terminal-table'
|
|
2
|
+
require_relative 'session'
|
|
3
|
+
|
|
4
|
+
module Fastlane
|
|
5
|
+
module DeveloperDevicePlatform
|
|
6
|
+
class SessionSummary
|
|
7
|
+
HEADINGS = ["JOB", "EXECUTION", "RESULT"].freeze
|
|
8
|
+
|
|
9
|
+
attr_reader :session
|
|
10
|
+
|
|
11
|
+
def initialize(session)
|
|
12
|
+
@session = session
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def rows
|
|
16
|
+
session.jobs.flat_map do |job|
|
|
17
|
+
if job.executions.empty?
|
|
18
|
+
[[job.display_name, "", describe(job)]]
|
|
19
|
+
else
|
|
20
|
+
job.executions.map { |execution| [job.display_name, execution.display_name, describe(execution)] }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_s
|
|
26
|
+
lines = [headline, session.result_message]
|
|
27
|
+
lines << Terminal::Table.new(headings: HEADINGS, rows: rows).to_s unless rows.empty?
|
|
28
|
+
lines << "Result files: #{session.console_url}" if session.console_url
|
|
29
|
+
lines.compact.join("\n")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def headline
|
|
35
|
+
if session.done?
|
|
36
|
+
"Session #{session.id} finished with result #{session.result}"
|
|
37
|
+
else
|
|
38
|
+
"Session #{session.id} is #{session.status}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def describe(report)
|
|
43
|
+
return report.result if report.passed?
|
|
44
|
+
|
|
45
|
+
[report.result, report.message].compact.join(": ")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require 'fastlane/plugin/developer_device_platform/version'
|
|
2
|
+
|
|
3
|
+
module Fastlane
|
|
4
|
+
module DeveloperDevicePlatform
|
|
5
|
+
def self.all_classes
|
|
6
|
+
Dir[File.expand_path('**/{actions,helper}/*.rb', File.dirname(__FILE__))]
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
Fastlane::DeveloperDevicePlatform.all_classes.each do |current|
|
|
12
|
+
require current
|
|
13
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fastlane-plugin-developer_device_platform
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ntsk
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-11 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bundler
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 2.4.0
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: 5.0.0
|
|
23
|
+
type: :development
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: 2.4.0
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 5.0.0
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: fastlane
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 2.239.0
|
|
40
|
+
type: :development
|
|
41
|
+
prerelease: false
|
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 2.239.0
|
|
47
|
+
- !ruby/object:Gem::Dependency
|
|
48
|
+
name: pry
|
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.14'
|
|
54
|
+
type: :development
|
|
55
|
+
prerelease: false
|
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.14'
|
|
61
|
+
- !ruby/object:Gem::Dependency
|
|
62
|
+
name: rake
|
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '13.0'
|
|
68
|
+
type: :development
|
|
69
|
+
prerelease: false
|
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '13.0'
|
|
75
|
+
- !ruby/object:Gem::Dependency
|
|
76
|
+
name: rspec
|
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.12'
|
|
82
|
+
type: :development
|
|
83
|
+
prerelease: false
|
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.12'
|
|
89
|
+
- !ruby/object:Gem::Dependency
|
|
90
|
+
name: rspec_junit_formatter
|
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0.6'
|
|
96
|
+
type: :development
|
|
97
|
+
prerelease: false
|
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0.6'
|
|
103
|
+
- !ruby/object:Gem::Dependency
|
|
104
|
+
name: rubocop
|
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '1.50'
|
|
110
|
+
- - "<"
|
|
111
|
+
- !ruby/object:Gem::Version
|
|
112
|
+
version: '1.51'
|
|
113
|
+
type: :development
|
|
114
|
+
prerelease: false
|
|
115
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
116
|
+
requirements:
|
|
117
|
+
- - "~>"
|
|
118
|
+
- !ruby/object:Gem::Version
|
|
119
|
+
version: '1.50'
|
|
120
|
+
- - "<"
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: '1.51'
|
|
123
|
+
- !ruby/object:Gem::Dependency
|
|
124
|
+
name: rubocop-ast
|
|
125
|
+
requirement: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - "~>"
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: 1.38.0
|
|
130
|
+
type: :development
|
|
131
|
+
prerelease: false
|
|
132
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
133
|
+
requirements:
|
|
134
|
+
- - "~>"
|
|
135
|
+
- !ruby/object:Gem::Version
|
|
136
|
+
version: 1.38.0
|
|
137
|
+
- !ruby/object:Gem::Dependency
|
|
138
|
+
name: rubocop-performance
|
|
139
|
+
requirement: !ruby/object:Gem::Requirement
|
|
140
|
+
requirements:
|
|
141
|
+
- - "~>"
|
|
142
|
+
- !ruby/object:Gem::Version
|
|
143
|
+
version: '1.17'
|
|
144
|
+
type: :development
|
|
145
|
+
prerelease: false
|
|
146
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
147
|
+
requirements:
|
|
148
|
+
- - "~>"
|
|
149
|
+
- !ruby/object:Gem::Version
|
|
150
|
+
version: '1.17'
|
|
151
|
+
- !ruby/object:Gem::Dependency
|
|
152
|
+
name: rubocop-require_tools
|
|
153
|
+
requirement: !ruby/object:Gem::Requirement
|
|
154
|
+
requirements:
|
|
155
|
+
- - "~>"
|
|
156
|
+
- !ruby/object:Gem::Version
|
|
157
|
+
version: '0.1'
|
|
158
|
+
type: :development
|
|
159
|
+
prerelease: false
|
|
160
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
161
|
+
requirements:
|
|
162
|
+
- - "~>"
|
|
163
|
+
- !ruby/object:Gem::Version
|
|
164
|
+
version: '0.1'
|
|
165
|
+
- !ruby/object:Gem::Dependency
|
|
166
|
+
name: simplecov
|
|
167
|
+
requirement: !ruby/object:Gem::Requirement
|
|
168
|
+
requirements:
|
|
169
|
+
- - "~>"
|
|
170
|
+
- !ruby/object:Gem::Version
|
|
171
|
+
version: '0.22'
|
|
172
|
+
type: :development
|
|
173
|
+
prerelease: false
|
|
174
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
175
|
+
requirements:
|
|
176
|
+
- - "~>"
|
|
177
|
+
- !ruby/object:Gem::Version
|
|
178
|
+
version: '0.22'
|
|
179
|
+
description:
|
|
180
|
+
email: contact@ntsk.jp
|
|
181
|
+
executables: []
|
|
182
|
+
extensions: []
|
|
183
|
+
extra_rdoc_files: []
|
|
184
|
+
files:
|
|
185
|
+
- LICENSE
|
|
186
|
+
- README.md
|
|
187
|
+
- lib/fastlane/plugin/developer_device_platform.rb
|
|
188
|
+
- lib/fastlane/plugin/developer_device_platform/actions/developer_device_platform_action.rb
|
|
189
|
+
- lib/fastlane/plugin/developer_device_platform/helper/device_run_client.rb
|
|
190
|
+
- lib/fastlane/plugin/developer_device_platform/helper/gcloud.rb
|
|
191
|
+
- lib/fastlane/plugin/developer_device_platform/helper/instrumentation_command.rb
|
|
192
|
+
- lib/fastlane/plugin/developer_device_platform/helper/options.rb
|
|
193
|
+
- lib/fastlane/plugin/developer_device_platform/helper/session.rb
|
|
194
|
+
- lib/fastlane/plugin/developer_device_platform/helper/session_summary.rb
|
|
195
|
+
- lib/fastlane/plugin/developer_device_platform/version.rb
|
|
196
|
+
homepage: https://github.com/ntsk/fastlane-plugin-developer_device_platform
|
|
197
|
+
licenses:
|
|
198
|
+
- MIT
|
|
199
|
+
metadata:
|
|
200
|
+
rubygems_mfa_required: 'true'
|
|
201
|
+
post_install_message:
|
|
202
|
+
rdoc_options: []
|
|
203
|
+
require_paths:
|
|
204
|
+
- lib
|
|
205
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
206
|
+
requirements:
|
|
207
|
+
- - ">="
|
|
208
|
+
- !ruby/object:Gem::Version
|
|
209
|
+
version: '3.1'
|
|
210
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
211
|
+
requirements:
|
|
212
|
+
- - ">="
|
|
213
|
+
- !ruby/object:Gem::Version
|
|
214
|
+
version: '0'
|
|
215
|
+
requirements: []
|
|
216
|
+
rubygems_version: 3.5.22
|
|
217
|
+
signing_key:
|
|
218
|
+
specification_version: 4
|
|
219
|
+
summary: Run Android instrumentation tests on Google Cloud Developer Device Platform
|
|
220
|
+
(Device Run)
|
|
221
|
+
test_files: []
|