Skip to content

Commit 2cf026f

Browse files
kaibolaytagboola
andauthored
Support automated tests (#351)
* Support automated tests * Update version to 0.9.0.pre.1 --------- Co-authored-by: Tunde Agboola <tun.agboola@gmail.com>
1 parent 52678c4 commit 2cf026f

File tree

5 files changed

+279
-9
lines changed

5 files changed

+279
-9
lines changed

.rubocop.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ Metrics/LineLength:
9999
Metrics/ParameterLists:
100100
Max: 17
101101
Metrics/PerceivedComplexity:
102-
Max: 18
102+
Max: 20
103103
Style/GuardClause:
104104
Enabled: false
105105
Style/StringLiterals:

lib/fastlane/plugin/firebase_app_distribution/actions/firebase_app_distribution_action.rb

Lines changed: 146 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class FirebaseAppDistributionAction < Action
2222
DEFAULT_UPLOAD_TIMEOUT_SECONDS = 300
2323
UPLOAD_MAX_POLLING_RETRIES = 60
2424
UPLOAD_POLLING_INTERVAL_SECONDS = 5
25+
TEST_MAX_POLLING_RETRIES = 25
26+
TEST_POLLING_INTERVAL_SECONDS = 30
2527

2628
def self.run(params)
2729
params.values # to validate all inputs before looking for the ipa/apk/aab
@@ -37,11 +39,13 @@ def self.run(params)
3739
binary_type = binary_type_from_path(binary_path)
3840

3941
# TODO(lkellogg): This sets the send timeout for all POST requests made by the client, but
40-
# ideally the timeout should only apply to the binary upload
42+
# ideally the timeout should only apply to the binary upload
4143
init_google_api_client(params[:debug], timeout)
4244
authorization = get_authorization(params[:service_credentials_file], params[:firebase_cli_token], params[:service_credentials_json_data], params[:debug])
4345
client = Google::Apis::FirebaseappdistributionV1::FirebaseAppDistributionService.new
4446
client.authorization = authorization
47+
alpha_client = Google::Apis::FirebaseappdistributionV1alpha::FirebaseAppDistributionService.new
48+
alpha_client.authorization = authorization
4549

4650
# If binary is an AAB, get the AAB info for this app, which includes the integration state
4751
# and certificate data
@@ -80,6 +84,16 @@ def self.run(params)
8084
release = update_release(client, release)
8185
end
8286

87+
test_devices =
88+
get_value_from_value_or_file(params[:test_devices], params[:test_devices_file])
89+
if present?(test_devices)
90+
UI.message("🤖 Starting automated tests.")
91+
release_test = test_release(alpha_client, release, test_devices, params[:test_username], params[:test_password], params[:test_username_resource], params[:test_password_resource])
92+
unless params[:test_async]
93+
poll_test_finished(alpha_client, release_test.name)
94+
end
95+
end
96+
8397
testers = get_value_from_value_or_file(params[:testers], params[:testers_file])
8498
groups = get_value_from_value_or_file(params[:groups], params[:groups_file])
8599
emails = string_to_array(testers)
@@ -260,7 +274,7 @@ def self.upload_binary(app_name, binary_path, client, timeout)
260274
# it should return a long running operation object, so we make a
261275
# standard http call instead and convert it to a long running object
262276
# https://github.com/googleapis/google-api-ruby-client/blob/main/generated/google-apis-firebaseappdistribution_v1/lib/google/apis/firebaseappdistribution_v1/service.rb#L79
263-
# TODO(kbolay) Prefer client.upload_medium
277+
# TODO(kbolay): Prefer client.upload_medium
264278
response = client.http(
265279
:post,
266280
"https://firebaseappdistribution.googleapis.com/upload/v1/#{app_name}/releases:upload",
@@ -318,6 +332,99 @@ def self.distribute_release(client, release, request)
318332
end
319333
end
320334

335+
def self.test_release(alpha_client, release, test_devices, username = nil, password = nil, username_resource = nil, password_resource = nil)
336+
if username_resource.nil? ^ password_resource.nil?
337+
UI.user_error!("Username and password resource names for automated tests need to be specified together.")
338+
end
339+
field_hints = nil
340+
if !username_resource.nil? && !password_resource.nil?
341+
field_hints =
342+
Google::Apis::FirebaseappdistributionV1alpha::GoogleFirebaseAppdistroV1alphaLoginCredentialFieldHints.new(
343+
username_resource_name: username_resource,
344+
password_resource_name: password_resource
345+
)
346+
end
347+
348+
if username.nil? ^ password.nil?
349+
UI.user_error!("Username and password for automated tests need to be specified together.")
350+
end
351+
login_credential = nil
352+
if !username.nil? && !password.nil?
353+
login_credential =
354+
Google::Apis::FirebaseappdistributionV1alpha::GoogleFirebaseAppdistroV1alphaLoginCredential.new(
355+
username: username,
356+
password: password,
357+
field_hints: field_hints
358+
)
359+
else
360+
unless field_hints.nil?
361+
UI.user_error!("Must specify username and password for automated tests if resource names are set.")
362+
end
363+
end
364+
365+
device_executions = string_to_array(test_devices, ';').map do |td_string|
366+
td_hash = parse_test_device_string(td_string)
367+
Google::Apis::FirebaseappdistributionV1alpha::GoogleFirebaseAppdistroV1alphaDeviceExecution.new(
368+
device: Google::Apis::FirebaseappdistributionV1alpha::GoogleFirebaseAppdistroV1alphaTestDevice.new(
369+
model: td_hash['model'],
370+
version: td_hash['version'],
371+
orientation: td_hash['orientation'],
372+
locale: td_hash['locale']
373+
)
374+
)
375+
end
376+
377+
release_test =
378+
Google::Apis::FirebaseappdistributionV1alpha::GoogleFirebaseAppdistroV1alphaReleaseTest.new(
379+
login_credential: login_credential,
380+
device_executions: device_executions
381+
)
382+
alpha_client.create_project_app_release_test(release.name, release_test)
383+
rescue Google::Apis::Error => err
384+
UI.crash!(err)
385+
end
386+
387+
def self.poll_test_finished(alpha_client, release_test_name)
388+
TEST_MAX_POLLING_RETRIES.times do
389+
UI.message("⏳ Waiting for test(s) to complete…")
390+
sleep(TEST_POLLING_INTERVAL_SECONDS)
391+
release_test = alpha_client.get_project_app_release_test(release_test_name)
392+
if release_test.device_executions.all? { |e| e.state == 'PASSED' }
393+
UI.success("✅ Passed automated test(s).")
394+
return
395+
end
396+
release_test.device_executions.each do |de|
397+
case de.state
398+
when 'PASSED', 'IN_PROGRESS'
399+
next
400+
when 'FAILED'
401+
UI.test_failure!("Automated test failed for #{device_to_s(de.device)}: #{de.failed_reason}.")
402+
when 'INCONCLUSIVE'
403+
UI.test_failure!("Automated test inconclusive for #{device_to_s(de.device)}: #{de.inconclusive_reason}.")
404+
else
405+
UI.test_failure!("Unsupported automated test state for #{device_to_s(de.device)}: #{de.state}.")
406+
end
407+
end
408+
end
409+
UI.test_failure!("Tests are running longer than expected.")
410+
end
411+
412+
def self.parse_test_device_string(td_string)
413+
allowed_keys = %w[model version locale orientation]
414+
key_value_pairs = td_string.split(',').map do |key_value_string|
415+
key, value = key_value_string.split('=')
416+
unless allowed_keys.include?(key)
417+
UI.user_error!("Unrecognized key in test_devices. Can only contain keys #{allowed_keys.join(', ')}.")
418+
end
419+
[key, value]
420+
end
421+
Hash[key_value_pairs]
422+
end
423+
424+
def self.device_to_s(device)
425+
"#{device.model} (#{device.version}/#{device.orientation}/#{device.locale})"
426+
end
427+
321428
def self.available_options
322429
[
323430
# iOS Specific
@@ -358,7 +465,7 @@ def self.available_options
358465
FastlaneCore::ConfigItem.new(key: :firebase_cli_path,
359466
deprecated: "This plugin no longer uses the Firebase CLI",
360467
env_name: "FIREBASEAPPDISTRO_FIREBASE_CLI_PATH",
361-
description: "The absolute path of the firebase cli command",
468+
description: "Absolute path of the Firebase CLI command",
362469
type: String),
363470
FastlaneCore::ConfigItem.new(key: :debug,
364471
description: "Print verbose debug output",
@@ -374,7 +481,7 @@ def self.available_options
374481
type: Integer),
375482
FastlaneCore::ConfigItem.new(key: :groups,
376483
env_name: "FIREBASEAPPDISTRO_GROUPS",
377-
description: "The group aliases used for distribution, separated by commas",
484+
description: "Group aliases used for distribution, separated by commas",
378485
optional: true,
379486
type: String),
380487
FastlaneCore::ConfigItem.new(key: :groups_file,
@@ -403,6 +510,39 @@ def self.available_options
403510
optional: true,
404511
type: String),
405512

513+
# Release Testing
514+
FastlaneCore::ConfigItem.new(key: :test_devices,
515+
env_name: "FIREBASEAPPDISTRO_TEST_DEVICES",
516+
description: "List of devices to run automated tests on, in the format 'model=<model-id>,version=<os-version-id>,locale=<locale>,orientation=<orientation>;model=<model-id>,...'. Run 'gcloud firebase test android|ios models list' to see available devices",
517+
optional: true,
518+
type: String),
519+
FastlaneCore::ConfigItem.new(key: :test_devices_file,
520+
env_name: "FIREBASEAPPDISTRO_TEST_DEVICES_FILE",
521+
description: "Path to file containing a list of devices to run automated tests on, in the format 'model=<model-id>,version=<os-version-id>,locale=<locale>,orientation=<orientation>;model=<model-id>,...'. Run 'gcloud firebase test android|ios models list' to see available devices",
522+
optional: true,
523+
type: String),
524+
FastlaneCore::ConfigItem.new(key: :test_username,
525+
description: "Username for automatic login",
526+
optional: true,
527+
type: String),
528+
FastlaneCore::ConfigItem.new(key: :test_password,
529+
description: "Password for automatic login",
530+
optional: true,
531+
type: String),
532+
FastlaneCore::ConfigItem.new(key: :test_username_resource,
533+
description: "Resource name of the username field for automatic login",
534+
optional: true,
535+
type: String),
536+
FastlaneCore::ConfigItem.new(key: :test_password_resource,
537+
description: "Resource name of the password field for automatic login",
538+
optional: true,
539+
type: String),
540+
FastlaneCore::ConfigItem.new(key: :test_async,
541+
description: "Don't wait for automatic test results",
542+
optional: false,
543+
default_value: false,
544+
type: Boolean),
545+
406546
# Auth
407547
FastlaneCore::ConfigItem.new(key: :firebase_cli_token,
408548
description: "Auth token generated using the Firebase CLI's login:ci command",
@@ -432,7 +572,8 @@ def self.example_code
432572
<<-CODE
433573
firebase_app_distribution(
434574
app: "<your Firebase app ID>",
435-
testers: "snatchev@google.com, rebeccahe@google.com"
575+
testers: "snatchev@google.com, rebeccahe@google.com",
576+
test_devices: "model=shiba,version=34,locale=en,orientation=portrait;model=b0q,version=33,locale=en,orientation=portrait",
436577
)
437578
CODE
438579
]

lib/fastlane/plugin/firebase_app_distribution/helper/firebase_app_distribution_helper.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ def get_value_from_value_or_file(value, path)
2727

2828
# Returns the array representation of a string with trimmed comma
2929
# seperated values.
30-
def string_to_array(string)
30+
def string_to_array(string, delimiter = ",")
3131
return [] if string.nil?
3232
# Strip string and then strip individual values
33-
string.strip.split(",").map(&:strip)
33+
string.strip.split(delimiter).map(&:strip)
3434
end
3535

3636
def parse_plist(path)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module Fastlane
22
module FirebaseAppDistribution
3-
VERSION = "0.8.1"
3+
VERSION = "0.9.0.pre.1"
44
end
55
end

0 commit comments

Comments
 (0)