@@ -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 ]
0 commit comments