fastlane-plugin-developer_device_platform 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a99f79e05b0a9a125921c472bd1edac41dc27c87dac082aeef067c99a84586d3
4
- data.tar.gz: 7e1751408cf3527c2b1ab83d8f66680137904a0acee6d82b4946c37b9b4a9ac1
3
+ metadata.gz: 7d7beb8da7cf98d4f592a20ab3b957b2e5a059a6959f52b560beab245ae251a0
4
+ data.tar.gz: b526995df11c60771ff7653ff3a947c910ece216bce370a855ef3d67bc500ca5
5
5
  SHA512:
6
- metadata.gz: d8e4599ff167590dc18b132e314e79e7ad7f9d4333f94c86999655e440898c89ceac4494185d780cc5763ad44e37f589ba6a12a821f452a7eaed05a1235f12a8
7
- data.tar.gz: dacea5f0bc9b4b5596fa33bb75b799a525a355d3636f704a9733d97abed96fb3e7494ca3c552b65ec6773ba074668d9572c06fc1ac9166081e9f153bc5ec7ea2
6
+ metadata.gz: 7a7abb638d150564242a219cdaed48b953a3557e9521b1c58cc0a869a32d67366605f03f9fe20751473dc30337c3cbaec2c6808987996a625344758f56f042d7
7
+ data.tar.gz: 970889f67676b92e02a4103e370d95d3367b58f41b3698fe78c09d453e71b45f4d4fe7f23777690e46132f96329f6f9ff429a182b1fb8b6700b6d109e39051f2
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  Run Android instrumentation tests on [Google Cloud Developer Device Platform](https://docs.cloud.google.com/developer-device-platform) (Device Run) from fastlane.
7
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`.
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 wraps `gcloud beta device-run` so Android instrumentation tests can keep running from a fastlane lane.
9
9
 
10
10
  ## What the action does
11
11
 
@@ -17,7 +17,7 @@ Firebase Test Lab is [deprecated and shuts down on September 30, 2027](https://f
17
17
 
18
18
  ## Prerequisites
19
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).
20
+ - [Google Cloud CLI](https://cloud.google.com/sdk/docs/install) 580.0.0 or newer with the `beta` component on the machine running fastlane (`gcloud components update && gcloud components install beta`). On GitHub Actions, pin `version` in `google-github-actions/setup-gcloud`; its default `latest` can resolve to an older release that predates Device Run.
21
21
  - A Google Cloud project with billing enabled and the APIs turned on:
22
22
 
23
23
  ```sh
@@ -71,7 +71,7 @@ developer_device_platform(
71
71
 
72
72
  ### Options
73
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>`.
74
+ Options mirror the flags of `gcloud beta device-run sessions submit instrumentation`. Every option except the hash-valued ones (`additional_test_options`, `other_files_to_push`, `labels`) can also be set through the environment variable `DEVELOPER_DEVICE_PLATFORM_<OPTION>`; array values are comma-separated.
75
75
 
76
76
  | Option | Type | Description |
77
77
  | --- | --- | --- |
@@ -105,7 +105,7 @@ Options mirror the flags of `gcloud beta device-run sessions submit instrumentat
105
105
  | `flaky_test_attempts` | Integer | Attempts per shard. Default `1`. |
106
106
  | `flaky_test_parallel_retry` | Boolean | Retry in parallel instead of sequentially. |
107
107
  | `flaky_test_retry_level` | String | `shard` or `test`. |
108
- | `async` | Boolean | Return the session ID right after submission. Default `false`. |
108
+ | `async` | Boolean | Return the session ID right after submission; the output directory and console URL are still exposed. Default `false`. |
109
109
  | `download_dir` | String | Local directory to download the session artifacts into. |
110
110
  | `fail_on_failure` | Boolean | Fail the lane when the result is not `PASSED`. Default `true`. |
111
111
 
@@ -120,6 +120,7 @@ The action returns a `Fastlane::DeveloperDevicePlatform::Session` (or the sessio
120
120
  | `DEVELOPER_DEVICE_PLATFORM_OUTPUT_DIRECTORY` | `gs://BUCKET/automation/sessions/SESSION_ID` |
121
121
  | `DEVELOPER_DEVICE_PLATFORM_CONSOLE_URL` | Cloud Console link to the artifacts |
122
122
  | `DEVELOPER_DEVICE_PLATFORM_DOWNLOAD_DIR` | Local artifact directory when `download_dir` is set |
123
+ | `DEVELOPER_DEVICE_PLATFORM_JUNIT_FILES` | Local `junit.xml` paths, one per job, when `download_dir` is set |
123
124
 
124
125
  ```ruby
125
126
  session = developer_device_platform(devices: ["shiba-34"], test_apk: "test.apk", fail_on_failure: false)
@@ -128,7 +129,9 @@ session.jobs.each { |job| puts "#{job.display_name}: #{job.result} #{job.message
128
129
 
129
130
  ### Results
130
131
 
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
+ Artifacts land in `gs://BUCKET/automation/sessions/SESSION_ID/job-NNN/`: a `junit.xml` per job, and `test_result.xml`, `instrument.log`, `logcat.txt`, video recordings and pulled files under `execution-NNN/`. With `download_dir` the same tree is copied to `<download_dir>/<SESSION_ID>/` and the `junit.xml` paths are exposed in `DEVELOPER_DEVICE_PLATFORM_JUNIT_FILES` for JUnit reporters.
133
+
134
+ Interrupting fastlane while it waits (Ctrl-C, or a cancelled CI job) cancels the Device Run session before exiting, so no job keeps running unattended.
132
135
 
133
136
  ### Authentication
134
137
 
@@ -136,23 +139,6 @@ On CI, the simplest setup is a service account key passed through `service_accou
136
139
 
137
140
  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
141
 
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
142
  ## Development
157
143
 
158
144
  ```sh
@@ -13,6 +13,7 @@ module Fastlane
13
13
  DEVELOPER_DEVICE_PLATFORM_OUTPUT_DIRECTORY = :DEVELOPER_DEVICE_PLATFORM_OUTPUT_DIRECTORY
14
14
  DEVELOPER_DEVICE_PLATFORM_CONSOLE_URL = :DEVELOPER_DEVICE_PLATFORM_CONSOLE_URL
15
15
  DEVELOPER_DEVICE_PLATFORM_DOWNLOAD_DIR = :DEVELOPER_DEVICE_PLATFORM_DOWNLOAD_DIR
16
+ DEVELOPER_DEVICE_PLATFORM_JUNIT_FILES = :DEVELOPER_DEVICE_PLATFORM_JUNIT_FILES
16
17
  end
17
18
 
18
19
  class DeveloperDevicePlatformAction < Action
@@ -31,11 +32,12 @@ module Fastlane
31
32
  session_id = client.submit(build_command(params)) { |line| UI.message(line) }
32
33
  lane_context[SharedValues::DEVELOPER_DEVICE_PLATFORM_SESSION_ID] = session_id
33
34
  UI.success("Created Device Run session #{session_id}")
34
- return session_id if params[:async]
35
+ if params[:async]
36
+ publish(client.describe(session_id, location: location))
37
+ return session_id
38
+ end
35
39
 
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)
40
+ session = await(client, session_id, location)
39
41
  publish(session)
40
42
  download(client, session, params[:download_dir])
41
43
  UI.message(DeveloperDevicePlatform::SessionSummary.new(session).to_s)
@@ -56,6 +58,23 @@ module Fastlane
56
58
  DeveloperDevicePlatform::InstrumentationCommand.new(**COMMAND_OPTIONS.to_h { |key| [key, params[key]] })
57
59
  end
58
60
 
61
+ def self.await(client, session_id, location)
62
+ UI.message("Waiting for session #{session_id} to complete...")
63
+ client.wait(session_id, location: location) { |line| UI.message(line) }
64
+ client.describe(session_id, location: location)
65
+ rescue SignalException
66
+ cancel(client, session_id, location)
67
+ raise
68
+ end
69
+
70
+ def self.cancel(client, session_id, location)
71
+ UI.important("Interrupted. Cancelling Device Run session #{session_id}...")
72
+ client.cancel(session_id, location: location)
73
+ UI.important("Cancelled Device Run session #{session_id}")
74
+ rescue *RECOVERABLE_ERRORS => e
75
+ UI.error("Could not cancel Device Run session #{session_id}: #{e.message}")
76
+ end
77
+
59
78
  def self.publish(session)
60
79
  lane_context[SharedValues::DEVELOPER_DEVICE_PLATFORM_RESULT] = session.result
61
80
  lane_context[SharedValues::DEVELOPER_DEVICE_PLATFORM_OUTPUT_DIRECTORY] = session.output_directory
@@ -68,6 +87,7 @@ module Fastlane
68
87
  UI.message("Downloading results of session #{session.id} to #{destination}...")
69
88
  local_path = client.download(session, destination) { |line| UI.verbose(line) }
70
89
  lane_context[SharedValues::DEVELOPER_DEVICE_PLATFORM_DOWNLOAD_DIR] = local_path
90
+ lane_context[SharedValues::DEVELOPER_DEVICE_PLATFORM_JUNIT_FILES] = Dir.glob(File.join(local_path, "**", "junit.xml"))
71
91
  UI.success("Results downloaded to #{local_path}")
72
92
  end
73
93
 
@@ -110,7 +130,8 @@ module Fastlane
110
130
  ["DEVELOPER_DEVICE_PLATFORM_RESULT", "Overall result (PASSED, FAILED, ERROR, TIMED_OUT, CANCELLED)"],
111
131
  ["DEVELOPER_DEVICE_PLATFORM_OUTPUT_DIRECTORY", "Cloud Storage directory (gs://...) holding the session artifacts"],
112
132
  ["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"]
133
+ ["DEVELOPER_DEVICE_PLATFORM_DOWNLOAD_DIR", "Local directory the artifacts were downloaded to, when download_dir is set"],
134
+ ["DEVELOPER_DEVICE_PLATFORM_JUNIT_FILES", "Downloaded junit.xml paths, one per job, when download_dir is set"]
114
135
  ]
115
136
  end
116
137
 
@@ -155,7 +176,7 @@ module Fastlane
155
176
  DeveloperDevicePlatform::Options.all
156
177
  end
157
178
 
158
- private_class_method :build_gcloud, :build_command, :publish, :download, :conclude
179
+ private_class_method :build_gcloud, :build_command, :await, :cancel, :publish, :download, :conclude
159
180
  end
160
181
  end
161
182
  end
@@ -8,6 +8,15 @@ module Fastlane
8
8
  class Error < StandardError; end
9
9
 
10
10
  SESSION_ANNOUNCEMENT = /Creating session \[([^\]]+)\]/
11
+ MINIMUM_GCLOUD_VERSION = "580.0.0".freeze
12
+
13
+ ENVIRONMENT_HINTS = {
14
+ /Invalid choice: 'device-run'/ =>
15
+ "The installed gcloud does not provide the device-run command group. " \
16
+ "Device Run requires Google Cloud CLI #{MINIMUM_GCLOUD_VERSION} or newer; run `gcloud components update`.",
17
+ /installation of components: \[beta\]/ =>
18
+ "The gcloud beta component is not installed; run `gcloud components install beta`."
19
+ }.freeze
11
20
 
12
21
  attr_reader :gcloud
13
22
 
@@ -26,6 +35,11 @@ module Fastlane
26
35
  nil
27
36
  end
28
37
 
38
+ def cancel(session_id, location:)
39
+ device_run("sessions", "cancel", session_id, "--location=#{location}")
40
+ nil
41
+ end
42
+
29
43
  def describe(session_id, location:)
30
44
  output = device_run("sessions", "describe", session_id, "--location=#{location}", "--full", "--format=json")
31
45
  Session.parse(output.stdout)
@@ -44,6 +58,15 @@ module Fastlane
44
58
 
45
59
  def device_run(*args, &progress)
46
60
  gcloud.run("beta", "device-run", *args, &progress)
61
+ rescue Gcloud::CommandError => e
62
+ hint = environment_hint(e.stderr)
63
+ raise e unless hint
64
+
65
+ raise Error, "#{hint}\n#{e.message}"
66
+ end
67
+
68
+ def environment_hint(stderr)
69
+ ENVIRONMENT_HINTS.find { |pattern, _| pattern.match?(stderr) }&.last
47
70
  end
48
71
  end
49
72
  end
@@ -51,7 +51,7 @@ module Fastlane
51
51
  stderr_text = +""
52
52
  stderr.each_line do |line|
53
53
  stderr_text << line
54
- yield(line.chomp) if block_given?
54
+ yield(line.chomp) if block_given? && !line.strip.empty?
55
55
  end
56
56
  [stdout_reader.value, stderr_text, waiter.value]
57
57
  end
@@ -83,7 +83,7 @@ module Fastlane
83
83
  def option(key, description, type: String, optional: true, default_value: nil, verify_block: nil)
84
84
  FastlaneCore::ConfigItem.new(
85
85
  key: key,
86
- env_name: "DEVELOPER_DEVICE_PLATFORM_#{key.to_s.upcase}",
86
+ env_name: env_name_for(key, type),
87
87
  description: description,
88
88
  type: type,
89
89
  optional: optional,
@@ -92,6 +92,12 @@ module Fastlane
92
92
  )
93
93
  end
94
94
 
95
+ def env_name_for(key, type)
96
+ return nil if type == Hash
97
+
98
+ "DEVELOPER_DEVICE_PLATFORM_#{key.to_s.upcase}"
99
+ end
100
+
95
101
  def enum_option(key, description, values)
96
102
  option(key, "#{description} (#{values.join(' | ')})", verify_block: proc do |value|
97
103
  UI.user_error!("#{key} must be one of #{values.join(', ')}, got '#{value}'") unless values.include?(value)
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module DeveloperDevicePlatform
3
- VERSION = "0.1.0".freeze
3
+ VERSION = "0.2.0".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-developer_device_platform
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ntsk