diff --git a/lib/fastlane/plugin/firebase_app_distribution/actions/firebase_app_distribution_action.rb b/lib/fastlane/plugin/firebase_app_distribution/actions/firebase_app_distribution_action.rb index 0cb0e49..8ac4365 100644 --- a/lib/fastlane/plugin/firebase_app_distribution/actions/firebase_app_distribution_action.rb +++ b/lib/fastlane/plugin/firebase_app_distribution/actions/firebase_app_distribution_action.rb @@ -57,7 +57,7 @@ def self.run(params) binary_type = binary_type_from_path(binary_path) UI.message("📡 Uploading the #{binary_type}.") - operation = upload_binary(app_name, binary_path, binary_type, client, timeout) + operation = upload_binary(client, app_name, binary_path, binary_type, timeout) UI.message("🕵️ Validating upload…") release = poll_upload_release_operation(client, operation, binary_type) @@ -88,11 +88,12 @@ def self.run(params) test_devices = get_value_from_value_or_file(params[:test_devices], params[:test_devices_file]) if present?(test_devices) - UI.message("🤖 Starting automated tests. Note: This feature is in beta.") + test_cases = + string_to_array(get_value_from_value_or_file(params[:test_case_ids], params[:test_case_ids_file]))&.map { |id| "#{app_name}/testCases/#{id}" } test_password = test_password_from_params(params) - release_test = test_release(alpha_client, release, test_devices, params[:test_username], test_password, params[:test_username_resource], params[:test_password_resource]) + release_tests = test_release(alpha_client, release, test_devices, test_cases, params[:test_username], test_password, params[:test_username_resource], params[:test_password_resource]) unless params[:test_non_blocking] - poll_test_finished(alpha_client, release_test.name) + poll_test_finished(alpha_client, release_tests) end end @@ -279,7 +280,7 @@ def self.poll_upload_release_operation(client, operation, binary_type) extract_release(operation) end - def self.upload_binary(app_name, binary_path, binary_type, client, timeout) + def self.upload_binary(client, app_name, binary_path, binary_type, timeout) options = Google::Apis::RequestOptions.new options.max_elapsed_time = timeout # includes retries (default = no retries) options.header = { @@ -359,7 +360,10 @@ def self.distribute_release(client, release, request) end end - def self.test_release(alpha_client, release, test_devices, username = nil, password = nil, username_resource = nil, password_resource = nil) + def self.test_release(alpha_client, release, test_devices, test_cases, username = nil, password = nil, username_resource = nil, password_resource = nil) + if present?(test_cases) && (!username_resource.nil? || !password_resource.nil?) + UI.user_error!("Password and username resource names are not supported for the AI testing agent.") + end if username_resource.nil? ^ password_resource.nil? UI.user_error!("Username and password resource names for automated tests need to be specified together.") end @@ -401,37 +405,64 @@ def self.test_release(alpha_client, release, test_devices, username = nil, passw ) end + UI.message("🤖 Starting automated tests. Note: This feature is in beta.") + release_tests = [] + if present?(test_cases) + test_cases.each do |tc| + release_tests.push(create_release_test(alpha_client, release.name, device_executions, login_credential, tc)) + end + else + release_tests.push(create_release_test(alpha_client, release.name, device_executions, login_credential)) + end + release_tests + end + + def self.create_release_test(alpha_client, release_name, device_executions, login_credential, test_case_name = nil) release_test = Google::Apis::FirebaseappdistributionV1alpha::GoogleFirebaseAppdistroV1alphaReleaseTest.new( + device_executions: device_executions, login_credential: login_credential, - device_executions: device_executions + test_case: test_case_name ) - alpha_client.create_project_app_release_test(release.name, release_test) + alpha_client.create_project_app_release_test(release_name, release_test) rescue Google::Apis::Error => err - UI.crash!(err) + case err.status_code.to_i + when 404 + UI.user_error!("Test Case #{test_case_name} not found") + else + UI.crash!(err) + end end - def self.poll_test_finished(alpha_client, release_test_name) + def self.poll_test_finished(alpha_client, release_tests) + release_test_names = release_tests.map(&:name) TEST_MAX_POLLING_RETRIES.times do - UI.message("⏳ The automated test results are pending.") + UI.message("⏳ #{release_test_names.size} automated test results are pending.") sleep(TEST_POLLING_INTERVAL_SECONDS) - release_test = alpha_client.get_project_app_release_test(release_test_name) - if release_test.device_executions.all? { |e| e.state == 'PASSED' } - UI.success("✅ Passed automated test(s).") - return - end - release_test.device_executions.each do |de| - case de.state - when 'PASSED', 'IN_PROGRESS' - next - when 'FAILED' - UI.test_failure!("Automated test failed for #{device_to_s(de.device)}: #{de.failed_reason}.") - when 'INCONCLUSIVE' - UI.test_failure!("Automated test inconclusive for #{device_to_s(de.device)}: #{de.inconclusive_reason}.") + release_test_names.delete_if do |release_test_name| + release_test = alpha_client.get_project_app_release_test(release_test_name) + if release_test.device_executions.all? { |e| e.state == 'PASSED' } + true else - UI.test_failure!("Unsupported automated test state for #{device_to_s(de.device)}: #{de.state}.") + release_test.device_executions.each do |de| + case de.state + when 'PASSED', 'IN_PROGRESS' + next + when 'FAILED' + UI.test_failure!("Automated test failed for #{device_to_s(de.device)}: #{de.failed_reason}.") + when 'INCONCLUSIVE' + UI.test_failure!("Automated test inconclusive for #{device_to_s(de.device)}: #{de.inconclusive_reason}.") + else + UI.test_failure!("Unsupported automated test state for #{device_to_s(de.device)}: #{de.state}.") + end + end + false end end + if release_test_names.empty? + UI.success("✅ Passed automated test(s).") + return + end end UI.test_failure!("It took longer than expected to process your test, please try again.") end @@ -580,6 +611,16 @@ def self.available_options optional: false, default_value: false, type: Boolean), + FastlaneCore::ConfigItem.new(key: :test_case_ids, + env_name: "FIREBASEAPPDISTRO_TEST_CASE_IDS", + description: "Test Case IDs, separated by commas. Note: This feature is in beta", + optional: true, + type: String), + FastlaneCore::ConfigItem.new(key: :test_case_ids_file, + env_name: "FIREBASEAPPDISTRO_TEST_CASE_IDS_FILE", + description: "Path to file with containing Test Case IDs, separated by commas or newlines. Note: This feature is in beta", + optional: true, + type: String), # Auth FastlaneCore::ConfigItem.new(key: :firebase_cli_token, diff --git a/lib/fastlane/plugin/firebase_app_distribution/helper/firebase_app_distribution_apis.rb b/lib/fastlane/plugin/firebase_app_distribution/helper/firebase_app_distribution_apis.rb index 3ab923b..91382a7 100644 --- a/lib/fastlane/plugin/firebase_app_distribution/helper/firebase_app_distribution_apis.rb +++ b/lib/fastlane/plugin/firebase_app_distribution/helper/firebase_app_distribution_apis.rb @@ -1,2 +1,48 @@ require 'google/apis/firebaseappdistribution_v1' require 'google/apis/firebaseappdistribution_v1alpha' + +# This is partially copied from google/apis/firebaseappdistribution_v1alpha v0.9.0 (2024-12-08) based discovery document revision 20241204. +# We can't depend on that version directly as long as fastlane locks google-cloud-env < 2.0.0 (to support Ruby 2.6). +# Newer versions of the API clients depend on google-apis-core >= 0.15.0 which depends on googleauth ~> 1.9 which depends on google-cloud-env ~> 2.1. +# See also https://github.com/fastlane/fastlane/pull/21685#pullrequestreview-2490037163 +module Google + module Apis + module FirebaseappdistributionV1alpha + class GoogleFirebaseAppdistroV1alphaReleaseTest + include Google::Apis::Core::Hashable + + attr_accessor :create_time + attr_accessor :device_executions + attr_accessor :display_name + attr_accessor :login_credential + attr_accessor :name + attr_accessor :test_case + attr_accessor :test_state + + def initialize(**args) + update!(**args) + end + + def update!(**args) + @create_time = args[:create_time] if args.key?(:create_time) + @device_executions = args[:device_executions] if args.key?(:device_executions) + @display_name = args[:display_name] if args.key?(:display_name) + @login_credential = args[:login_credential] if args.key?(:login_credential) + @name = args[:name] if args.key?(:name) + @test_case = args[:test_case] if args.key?(:test_case) + @test_state = args[:test_state] if args.key?(:test_state) + end + + class Representation < Google::Apis::Core::JsonRepresentation + property :create_time, as: 'createTime' + collection :device_executions, as: 'deviceExecutions', class: Google::Apis::FirebaseappdistributionV1alpha::GoogleFirebaseAppdistroV1alphaDeviceExecution, decorator: Google::Apis::FirebaseappdistributionV1alpha::GoogleFirebaseAppdistroV1alphaDeviceExecution::Representation + property :display_name, as: 'displayName' + property :login_credential, as: 'loginCredential', class: Google::Apis::FirebaseappdistributionV1alpha::GoogleFirebaseAppdistroV1alphaLoginCredential, decorator: Google::Apis::FirebaseappdistributionV1alpha::GoogleFirebaseAppdistroV1alphaLoginCredential::Representation + property :name, as: 'name' + property :test_case, as: 'testCase' + property :test_state, as: 'testState' + end + end + end + end +end diff --git a/lib/fastlane/plugin/firebase_app_distribution/version.rb b/lib/fastlane/plugin/firebase_app_distribution/version.rb index f3c5c54..3b4dffc 100644 --- a/lib/fastlane/plugin/firebase_app_distribution/version.rb +++ b/lib/fastlane/plugin/firebase_app_distribution/version.rb @@ -1,5 +1,5 @@ module Fastlane module FirebaseAppDistribution - VERSION = "0.9.1" + VERSION = "0.10.0" end end diff --git a/spec/firebase_app_distribution_action_spec.rb b/spec/firebase_app_distribution_action_spec.rb index 7808607..1c652e7 100644 --- a/spec/firebase_app_distribution_action_spec.rb +++ b/spec/firebase_app_distribution_action_spec.rb @@ -625,11 +625,24 @@ def stub_get_aab_info(integration_state = 'INTEGRATED') end devices = 'model=model1,version=version1,locale=locale1,orientation=orientation1;version=version2,model=model2,orientation=orientation2,locale=locale2' action.run({ - app: android_app_id, - android_artifact_path: 'path/to.apk', - test_devices: devices, - test_non_blocking: true - }) + app: android_app_id, + android_artifact_path: 'path/to.apk', + test_devices: devices, + test_non_blocking: true + }) + end + + it 'passes test case IDs' do + allow_any_instance_of(Google::Apis::FirebaseappdistributionV1alpha::FirebaseAppDistributionService).to receive(:create_project_app_release_test) do |_, release_name, request| + expect(["#{android_app_name}/testCases/foo", "#{android_app_name}/testCases/bar", "#{android_app_name}/testCases/baz"]).to include(request.test_case) + end + action.run({ + app: android_app_id, + android_artifact_path: 'path/to.apk', + test_devices: 'model=model1,version=version1,locale=locale1,orientation=orientation1', + test_case_ids: "foo, bar, baz", + test_non_blocking: true + }) end end end