Skip to content

Commit 8bdd743

Browse files
Add Firebase Test Lab Support (#355)
* Add Android Firebase Action + Helper * Move Firebase Device out of the Helper * Move log file parsing out of the FirebaseHelper The Firebase Helper had a lot of pieces doing a variety of things – splitting this into model objects makes it easier to change and test. * Add CHANGELOG note * Cleanup * Apply suggestions from code review Co-authored-by: Olivier Halligon <olivier.halligon@automattic.com> * Fix tests * Don’t authenticate in constructor * Verify gcloud binary on FirebaseTestRunner.new * Make CI happy * Remove the explicit verify_has_gloud_binary call * Simplify more_details_url Co-authored-by: Olivier Halligon <olivier.halligon@automattic.com> * Simplify raw_results_paths Co-authored-by: Olivier Halligon <olivier.halligon@automattic.com> * Adjust `require` * Add docs * Fix broken success? method * Fix `passed?` * Fix Google Cloud Storage gem pinning * Apply suggestions from code review Co-authored-by: Olivier Halligon <olivier.halligon@automattic.com> * Fix tests * Simplify parameters * Indentation Fix Co-authored-by: Olivier Halligon <olivier.halligon@automattic.com> * Fix an oops * Re-add default_value_dynamic * Fix unclear errors when gcloud binary missing * Escape all the things * Improve Firebase Test Runner Errors * Remove constants * Fix lint issues * Fix rubocop error * Document the `env_name` * Remove key file environment variable setting * Improve argument documentation * Avoid double escaping * Apply suggestions from code review Co-authored-by: Olivier Halligon <olivier.halligon@automattic.com> * Fix crashes * Check params early to avoid late prompts * Add Action Validation Tests * Fix params * Add Firebase Login Action * Remove login logic from Firebase_Test Action * Put back key file parameter * Make FirebaseTestRunner static * Manually specify the project_id field Co-authored-by: Olivier Halligon <olivier.halligon@automattic.com>
1 parent 801fecf commit 8bdd743

26 files changed

+5768
-5
lines changed

.rubocop.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Metrics/ClassLength:
6767
Max: 300
6868

6969
Metrics/MethodLength:
70-
Max: 100
70+
Max: 150
7171

7272
Metrics/ModuleLength:
7373
Max: 300

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
### New Features
1212

1313
* Introduce new `ios_send_app_size_metrics` and `android_send_app_size_metrics` actions. [#364] [#365]
14+
* Add the ability to run Firebase Test Lab tests. [#355]
1415

1516
### Bug Fixes
1617

Gemfile.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ PATH
88
chroma (= 0.2.0)
99
diffy (~> 3.3)
1010
git (~> 1.3)
11+
google-cloud-storage (~> 1.31)
1112
jsonlint (~> 0.3)
1213
nokogiri (~> 1.11)
1314
octokit (~> 4.18)

fastlane-plugin-wpmreleasetoolkit.gemspec

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ Gem::Specification.new do |spec|
3838
spec.add_dependency 'progress_bar', '~> 1.3'
3939
spec.add_dependency 'rake', '>= 12.3', '< 14.0'
4040
spec.add_dependency 'rake-compiler', '~> 1.0'
41+
42+
# `google-cloud-storage` is required by fastlane, but we pin it in case it's not in the future
43+
spec.add_dependency 'google-cloud-storage', '~> 1.31'
44+
4145
# Some of the upstream code uses `BigDecimal.new` which version 2.0 of the
4246
# `bigdecimal` gem removed. Until we'll find the time to identify the
4347
# dependencies and see if we can move them to something compatible with

lib/fastlane/plugin/wpmreleasetoolkit.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
module Fastlane
44
module Wpmreleasetoolkit
5-
# Return all .rb files inside the "actions" and "helper" directory
5+
# Return all .rb files inside the "actions", "helper" and "models" directories
66
def self.all_classes
7-
Dir[File.expand_path('**/{actions,helper}/**/*.rb', File.dirname(__FILE__))]
7+
Dir[File.expand_path('**/{actions,helper,models}/**/*.rb', File.dirname(__FILE__))]
88
end
99
end
1010
end
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
require 'securerandom'
2+
3+
module Fastlane
4+
module Actions
5+
module SharedValues
6+
FIREBASE_TEST_RESULT = :FIREBASE_TEST_LOG_FILE
7+
FIREBASE_TEST_LOG_FILE_PATH = :FIREBASE_TEST_LOG_FILE_PATH
8+
end
9+
10+
class AndroidFirebaseTestAction < Action
11+
def self.run(params)
12+
validate_options(params)
13+
14+
UI.user_error!('You must be logged in to Firebase prior to calling this action. Use the `FirebaseLogin` Action to log in if needed') unless Fastlane::FirebaseAccount.authenticated?
15+
16+
# Log in to Firebase (and validate credentials)
17+
run_uuid = params[:test_run_id] || SecureRandom.uuid
18+
test_dir = params[:results_output_dir] || File.join(Dir.tmpdir(), run_uuid)
19+
20+
# Set up the log file and output directory
21+
FileUtils.mkdir_p(test_dir)
22+
Fastlane::Actions.lane_context[:FIREBASE_TEST_LOG_FILE_PATH] = File.join(test_dir, 'output.log')
23+
24+
device = Fastlane::FirebaseDevice.new(
25+
model: params[:model],
26+
version: params[:version],
27+
locale: params[:locale],
28+
orientation: params[:orientation]
29+
)
30+
31+
result = FirebaseTestRunner.run_tests(
32+
project_id: params[:project_id],
33+
apk_path: params[:apk_path],
34+
test_apk_path: params[:test_apk_path],
35+
device: device,
36+
type: params[:type]
37+
)
38+
39+
# Download all of the outputs from the job to the local machine
40+
FirebaseTestRunner.download_result_files(
41+
result: result,
42+
destination: test_dir,
43+
project_id: params[:project_id],
44+
key_file_path: params[:key_file]
45+
)
46+
47+
FastlaneCore::UI.test_failure! "Firebase Tests failed – more information can be found at #{result.more_details_url}" unless result.success?
48+
49+
UI.success 'Firebase Tests Complete'
50+
end
51+
52+
# Fastlane doesn't eagerly validate options for us, so we'll do it first to have control over
53+
# when they're evalutated.
54+
def self.validate_options(params)
55+
available_options
56+
.reject { |opt| opt.optional || !opt.default_value.nil? }
57+
.map(&:key)
58+
.each { |k| params[k] }
59+
end
60+
61+
#####################################################
62+
# @!group Documentation
63+
#####################################################
64+
65+
def self.description
66+
'Runs the specified tests in Firebase Test Lab'
67+
end
68+
69+
def self.details
70+
description
71+
end
72+
73+
def self.available_options
74+
[
75+
FastlaneCore::ConfigItem.new(
76+
key: :project_id,
77+
# `env_name` comes from the Google Cloud default: https://cloud.google.com/functions/docs/configuring/env-var
78+
env_name: 'GCP_PROJECT',
79+
description: 'The Project ID to test in',
80+
type: String
81+
),
82+
FastlaneCore::ConfigItem.new(
83+
key: :key_file,
84+
description: 'The key file used to authorize with Google Cloud',
85+
type: String,
86+
verify_block: proc do |value|
87+
UI.user_error!('The `:key_file` parameter is required') if value.empty?
88+
UI.user_error!("No Google Cloud Key file found at: #{value}") unless File.exist?(value)
89+
end
90+
),
91+
FastlaneCore::ConfigItem.new(
92+
key: :apk_path,
93+
description: 'Path to the application APK on the local machine',
94+
type: String,
95+
verify_block: proc do |value|
96+
UI.user_error!('The `:apk_path` parameter is required') if value.empty?
97+
UI.user_error!("Invalid application APK: #{value}") unless File.exist?(value)
98+
end
99+
),
100+
FastlaneCore::ConfigItem.new(
101+
key: :test_apk_path,
102+
description: 'Path to the test bundle APK on the local machine',
103+
type: String,
104+
verify_block: proc do |value|
105+
UI.user_error!('The `:test_apk_path` parameter is required') if value.empty?
106+
UI.user_error!("Invalid test APK: #{value}") unless File.exist?(value)
107+
end
108+
),
109+
FastlaneCore::ConfigItem.new(
110+
key: :model,
111+
description: 'The device model to use to run the test',
112+
type: String,
113+
verify_block: proc do |value|
114+
UI.user_error!('The `:model` parameter is required') if value.empty?
115+
FirebaseTestRunner.verify_has_gcloud_binary!
116+
model_names = Fastlane::FirebaseDevice.valid_model_names
117+
UI.user_error!("Invalid Model Name: #{value}. Valid Model Names: #{model_names}") unless model_names.include?(value)
118+
end
119+
),
120+
FastlaneCore::ConfigItem.new(
121+
key: :version,
122+
description: 'The Android version (API Level) to use to run the test',
123+
type: Integer,
124+
verify_block: proc do |value|
125+
FirebaseTestRunner.verify_has_gcloud_binary!
126+
version_numbers = Fastlane::FirebaseDevice.valid_version_numbers
127+
UI.user_error!("Invalid Version Number: #{value}. Valid Version Numbers: #{version_numbers}") unless version_numbers.include?(value)
128+
end
129+
),
130+
FastlaneCore::ConfigItem.new(
131+
key: :locale,
132+
description: 'The locale code to use when running the test',
133+
type: String,
134+
default_value: 'en',
135+
verify_block: proc do |value|
136+
FirebaseTestRunner.verify_has_gcloud_binary!
137+
locale_codes = Fastlane::FirebaseDevice.valid_locales
138+
UI.user_error!("Invalid Locale: #{value}. Valid Locales: #{locale_codes}") unless locale_codes.include?(value)
139+
end
140+
),
141+
FastlaneCore::ConfigItem.new(
142+
key: :orientation,
143+
description: 'Which orientation to run the device in',
144+
type: String,
145+
default_value: 'portrait',
146+
verify_block: proc do |value|
147+
orientations = Fastlane::FirebaseDevice.valid_orientations
148+
UI.user_error!("Invalid Orientation: #{value}. Valid Orientations: #{orientations}") unless orientations.include?(value)
149+
end
150+
),
151+
FastlaneCore::ConfigItem.new(
152+
key: :type,
153+
description: 'The type of test to run (e.g. `instrumentation` or `robo`)',
154+
type: String,
155+
default_value: 'instrumentation',
156+
verify_block: proc do |value|
157+
types = Fastlane::FirebaseTestRunner::VALID_TEST_TYPES
158+
UI.user_error!("Invalid Test Type: #{value}. Valid Types: #{types}") unless types.include?(value)
159+
end
160+
),
161+
FastlaneCore::ConfigItem.new(
162+
key: :test_run_id,
163+
description: 'A unique ID used to identify this test run',
164+
default_value_dynamic: true,
165+
optional: true,
166+
type: String
167+
),
168+
FastlaneCore::ConfigItem.new(
169+
key: :results_output_dir,
170+
description: 'The path to the folder where we will store the results of this test run',
171+
default_value_dynamic: true,
172+
optional: true,
173+
type: String
174+
),
175+
]
176+
end
177+
178+
def self.authors
179+
['Automattic']
180+
end
181+
182+
def self.is_supported?(platform)
183+
platform == :android
184+
end
185+
end
186+
end
187+
end

lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,6 @@ def self.details
152152
DETAILS
153153
end
154154

155-
# rubocop:disable Metrics/MethodLength
156155
def self.available_options
157156
[
158157
FastlaneCore::ConfigItem.new(
@@ -259,7 +258,6 @@ def self.available_options
259258
),
260259
]
261260
end
262-
# rubocop:enable Metrics/MethodLength
263261

264262
def self.return_type
265263
:integer
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
require 'securerandom'
2+
3+
module Fastlane
4+
module Actions
5+
class FirebaseLoginAction < Action
6+
def self.run(params)
7+
Fastlane::FirebaseAccount.activate_service_account_with_key_file(params[:key_file])
8+
end
9+
10+
#####################################################
11+
# @!group Documentation
12+
#####################################################
13+
def self.description
14+
'Logs the local machine into Google Cloud using the provided key file'
15+
end
16+
17+
def self.details
18+
description
19+
end
20+
21+
def self.available_options
22+
[
23+
FastlaneCore::ConfigItem.new(
24+
key: :key_file,
25+
description: 'The key file used to authorize with Google Cloud',
26+
type: String,
27+
verify_block: proc do |value|
28+
UI.user_error!('The `:key_file` parameter is required') if value.empty?
29+
UI.user_error!("No Google Cloud Key file found at: #{value}") unless File.exist?(value)
30+
end
31+
),
32+
]
33+
end
34+
35+
def self.authors
36+
['Automattic']
37+
end
38+
39+
def self.is_supported?(platform)
40+
true
41+
end
42+
end
43+
end
44+
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module Fastlane
2+
class FirebaseAccount
3+
def self.activate_service_account_with_key_file(key_file_path)
4+
Fastlane::Actions.sh('gcloud', 'auth', 'activate-service-account', '--key-file', key_file_path)
5+
end
6+
7+
def self.authenticated?
8+
auth_status = JSON.parse(auth_status_data)
9+
auth_status.any? do |account|
10+
account['status'] == 'ACTIVE'
11+
end
12+
end
13+
14+
# Lookup the current account authentication status
15+
def self.auth_status_data
16+
Fastlane::Actions.sh('gcloud', 'auth', 'list', '--format', 'json', log: false)
17+
end
18+
end
19+
end
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
module Fastlane
2+
class FirebaseDevice
3+
attr_reader :model, :version, :locale, :orientation
4+
5+
def initialize(model:, version:, orientation:, locale: 'en')
6+
raise 'Invalid Model' unless FirebaseDevice.valid_model_names.include? model
7+
raise 'Invalid Version' unless FirebaseDevice.valid_version_numbers.include? version
8+
raise 'Invalid Locale' unless FirebaseDevice.valid_locales.include? locale
9+
raise 'Invalid Orientation' unless FirebaseDevice.valid_orientations.include? orientation
10+
11+
@model = model
12+
@version = version
13+
@locale = locale
14+
@orientation = orientation
15+
end
16+
17+
def to_s
18+
"model=#{@model},version=#{@version},locale=#{@locale},orientation=#{@orientation}"
19+
end
20+
21+
class << self
22+
@locale_data = nil
23+
@model_data = nil
24+
@version_data = nil
25+
26+
def valid_model_names
27+
JSON.parse(model_data).map { |device| device['codename'] }
28+
end
29+
30+
def valid_version_numbers
31+
JSON.parse(version_data).map { |version| version['apiLevel'].to_i }
32+
end
33+
34+
def valid_locales
35+
JSON.parse(locale_data).map { |locale| locale['id'] }
36+
end
37+
38+
def valid_orientations
39+
%w[portrait landscape]
40+
end
41+
42+
def locale_data
43+
FirebaseDevice.verify_logged_in!
44+
@locale_data ||= Fastlane::Actions.sh('gcloud', 'firebase', 'test', 'android', 'locales', 'list', '--format="json"', log: false)
45+
end
46+
47+
def model_data
48+
FirebaseDevice.verify_logged_in!
49+
@model_data ||= Fastlane::Actions.sh('gcloud', 'firebase', 'test', 'android', 'models', 'list', '--format="json"', log: false)
50+
end
51+
52+
def version_data
53+
FirebaseDevice.verify_logged_in!
54+
@version_data ||= Fastlane::Actions.sh('gcloud', 'firebase', 'test', 'android', 'versions', 'list', '--format="json"', log: false)
55+
end
56+
57+
def verify_logged_in!
58+
UI.user_error!('You must call `firebase_login` before creating a FirebaseDevice object') unless FirebaseAccount.authenticated?
59+
end
60+
end
61+
end
62+
end

0 commit comments

Comments
 (0)