class Fastlane::Actions::FirebaseTestLabIosXctestAction

Constants

DEFAULT_APP_BUNDLE_NAME
PULL_RESULT_INTERVAL
RUNNING_STATES

Public Class Methods

authors() click to toggle source
# File lib/fastlane/plugin/firebase_test_lab/actions/firebase_test_lab_ios_xctest.rb, line 266
def self.authors
  ["powerivq"]
end
available_options() click to toggle source
# File lib/fastlane/plugin/firebase_test_lab/actions/firebase_test_lab_ios_xctest.rb, line 262
def self.available_options
  Fastlane::FirebaseTestLab::Options.available_options
end
description() click to toggle source

@!group Documentation

# File lib/fastlane/plugin/firebase_test_lab/actions/firebase_test_lab_ios_xctest.rb, line 258
def self.description
  "Submit an iOS XCTest job to Firebase Test Lab"
end
extract_execution_results(execution_results) click to toggle source
# File lib/fastlane/plugin/firebase_test_lab/actions/firebase_test_lab_ios_xctest.rb, line 179
def self.extract_execution_results(execution_results)
  UI.message("Test job(s) are finalized")
  UI.message("-------------------------")
  UI.message("|   EXECUTION RESULTS   |")
  failures = 0
  execution_results["testExecutions"].each do |execution|
    UI.message("-------------------------")
    execution_info = "#{execution['id']}: #{execution['state']}"
    if execution["state"] != "FINISHED"
      failures += 1
      UI.error(execution_info)
    else
      UI.success(execution_info)
    end

    # Display build logs
    if !execution["testDetails"].nil? && !execution["testDetails"]["progressMessages"].nil?
      execution["testDetails"]["progressMessages"].each { |msg| UI.message(msg) }
    end
  end

  UI.message("-------------------------")
  if failures > 0
    UI.error("😞  #{failures} execution(s) have failed to complete.")
  else
    UI.success("🎉  All jobs have ran and completed.")
  end
  return failures == 0
end
extract_test_results(test_results, gcp_project, history_id, execution_id) click to toggle source
# File lib/fastlane/plugin/firebase_test_lab/actions/firebase_test_lab_ios_xctest.rb, line 209
def self.extract_test_results(test_results, gcp_project, history_id, execution_id)
  steps = test_results["steps"]
  failures = 0
  inconclusive_runs = 0

  UI.message("-------------------------")
  UI.message("|      TEST OUTCOME     |")
  steps.each do |step|
    UI.message("-------------------------")
    step_id = step["stepId"]
    UI.message("Test step: #{step_id}")

    run_duration_sec = step["runDuration"]["seconds"] || 0
    UI.message("Execution time: #{run_duration_sec} seconds")

    outcome = step["outcome"]["summary"]
    case outcome
    when "success"
      UI.success("Result: #{outcome}")
    when "skipped"
      UI.message("Result: #{outcome}")
    when "inconclusive"
      inconclusive_runs += 1
      UI.error("Result: #{outcome}")
    when "failure"
      failures += 1
      UI.error("Result: #{outcome}")
    end
    UI.message("For details, go to https://console.firebase.google.com/project/#{gcp_project}/testlab/" \
      "histories/#{history_id}/matrices/#{execution_id}/executions/#{step_id}")
  end

  UI.message("-------------------------")
  if failures == 0 && inconclusive_runs == 0
    UI.success("🎉  Yay! All executions are completed successfully!")
  end
  if failures > 0
    UI.error("😞  #{failures} step(s) have failed.")
  end
  if inconclusive_runs > 0
    UI.error("😞  #{inconclusive_runs} step(s) yielded inconclusive outcomes.")
  end
  return failures == 0 && inconclusive_runs == 0
end
generate_directory_name() click to toggle source
# File lib/fastlane/plugin/firebase_test_lab/actions/firebase_test_lab_ios_xctest.rb, line 163
def self.generate_directory_name
  timestamp = Time.now.getutc.strftime("%Y%m%d-%H%M%SZ")
  return "fastlane-#{timestamp}-#{SecureRandom.hex[0..5]}"
end
is_supported?(platform) click to toggle source
# File lib/fastlane/plugin/firebase_test_lab/actions/firebase_test_lab_ios_xctest.rb, line 270
def self.is_supported?(platform)
  return platform == :ios
end
run(params) click to toggle source
# File lib/fastlane/plugin/firebase_test_lab/actions/firebase_test_lab_ios_xctest.rb, line 24
def self.run(params)
  gcp_project = params[:gcp_project]
  gcp_requests_timeout = params[:gcp_requests_timeout]
  oauth_key_file_path = params[:oauth_key_file_path]
  gcp_credential = Fastlane::FirebaseTestLab::Credential.new(key_file_path: oauth_key_file_path)

  ftl_service = Fastlane::FirebaseTestLab::FirebaseTestLabService.new(gcp_credential)

  # The default Google Cloud Storage path we store app bundle and test results
  gcs_workfolder = generate_directory_name

  # Firebase Test Lab requires an app bundle be already on Google Cloud Storage before starting the job
  if params[:app_path].to_s.start_with?("gs://")
    # gs:// is a path on Google Cloud Storage, we do not need to re-upload the app to a different bucket
    app_gcs_link = params[:app_path]
  else
    FirebaseTestLab::IosValidator.validate_ios_app(params[:app_path])

    # When given a local path, we upload the app bundle to Google Cloud Storage
    upload_spinner = TTY::Spinner.new("[:spinner] Uploading the app to GCS...", format: :dots)
    upload_spinner.auto_spin
    upload_bucket_name = ftl_service.get_default_bucket(gcp_project)
    timeout = gcp_requests_timeout ? gcp_requests_timeout.to_i : nil
    app_gcs_link = upload_file(params[:app_path],
                               upload_bucket_name,
                               "#{gcs_workfolder}/#{DEFAULT_APP_BUNDLE_NAME}",
                               gcp_project,
                               gcp_credential,
                               timeout)
    upload_spinner.success("Done")
  end

  UI.message("Submitting job(s) to Firebase Test Lab")
  
  result_storage = (params[:result_storage] ||
    "gs://#{ftl_service.get_default_bucket(gcp_project)}/#{gcs_workfolder}")
  UI.message("Test Results bucket: #{result_storage}")
  
  # We have gathered all the information. Call Firebase Test Lab to start the job now
  matrix_id = ftl_service.start_job(gcp_project,
                                    app_gcs_link,
                                    result_storage,
                                    params[:devices],
                                    params[:timeout_sec],
                                    params[:gcp_additional_client_info])

  # In theory, matrix_id should be available. Keep it to catch unexpected Firebase Test Lab API response
  if matrix_id.nil?
    UI.abort_with_message!("No matrix ID received.")
  end
  UI.message("Matrix ID for this submission: #{matrix_id}")
  wait_for_test_results(ftl_service, gcp_project, matrix_id, params[:async])
end
try_get_history_id_and_execution_id(matrix_results) click to toggle source
# File lib/fastlane/plugin/firebase_test_lab/actions/firebase_test_lab_ios_xctest.rb, line 168
def self.try_get_history_id_and_execution_id(matrix_results)
  if matrix_results["resultStorage"].nil? || matrix_results["resultStorage"]["toolResultsExecution"].nil?
    return nil, nil
  end

  tool_results_execution = matrix_results["resultStorage"]["toolResultsExecution"]
  history_id = tool_results_execution["historyId"]
  execution_id = tool_results_execution["executionId"]
  return history_id, execution_id
end
upload_file(app_path, bucket_name, gcs_path, gcp_project, gcp_credential, gcp_requests_timeout) click to toggle source
# File lib/fastlane/plugin/firebase_test_lab/actions/firebase_test_lab_ios_xctest.rb, line 78
def self.upload_file(app_path, bucket_name, gcs_path, gcp_project, gcp_credential, gcp_requests_timeout)
  file_name = "gs://#{bucket_name}/#{gcs_path}"
  storage = Fastlane::FirebaseTestLab::Storage.new(gcp_project, gcp_credential, gcp_requests_timeout)
  storage.upload_file(File.expand_path(app_path), bucket_name, gcs_path)
  return file_name
end
wait_for_test_results(ftl_service, gcp_project, matrix_id, async) click to toggle source
# File lib/fastlane/plugin/firebase_test_lab/actions/firebase_test_lab_ios_xctest.rb, line 85
def self.wait_for_test_results(ftl_service, gcp_project, matrix_id, async)
  firebase_console_link = nil

  spinner = TTY::Spinner.new("[:spinner] Starting tests...", format: :dots)
  spinner.auto_spin

  # Keep pulling test results until they are ready
  loop do
    results = ftl_service.get_matrix_results(gcp_project, matrix_id)

    if firebase_console_link.nil?
      history_id, execution_id = try_get_history_id_and_execution_id(results)
      # Once we get the Firebase console link, we display that exactly once
      unless history_id.nil? || execution_id.nil?
        firebase_console_link = "https://console.firebase.google.com" \
          "/project/#{gcp_project}/testlab/histories/#{history_id}/matrices/#{execution_id}"

        spinner.success("Done")
        UI.message("Go to #{firebase_console_link} for more information about this run")

        if async
          UI.success("Job(s) have been submitted to Firebase Test Lab")
          return
        end

        spinner = TTY::Spinner.new("[:spinner] Waiting for results...", format: :dots)
        spinner.auto_spin
      end
    end

    state = results["state"]
    # Handle all known error statuses
    if FirebaseTestLab::ERROR_STATE_TO_MESSAGE.key?(state.to_sym)
      spinner.error("Failed")
      invalid_matrix_details = results["invalidMatrixDetails"]
      if invalid_matrix_details &&
         FirebaseTestLab::INVALID_MATRIX_DETAIL_TO_MESSAGE.key?(invalid_matrix_details.to_sym)
        UI.error(FirebaseTestLab::INVALID_MATRIX_DETAIL_TO_MESSAGE[invalid_matrix_details.to_sym])
      end
      UI.user_error!(FirebaseTestLab::ERROR_STATE_TO_MESSAGE[state.to_sym])
    end

    if state == "FINISHED"
      spinner.success("Done")
      # Inspect the execution results: only contain info on whether each job finishes.
      # Do not include whether tests fail
      executions_completed = extract_execution_results(results)

      if results["resultStorage"].nil? || results["resultStorage"]["toolResultsExecution"].nil?
        UI.abort_with_message!("Unexpected response from Firebase test lab: Cannot retrieve result info")
      end

      # Now, look at the actual test result and see if they succeed
      history_id, execution_id = try_get_history_id_and_execution_id(results)
      if history_id.nil? || execution_id.nil?
        UI.abort_with_message!("Unexpected response from Firebase test lab: No history or execution ID")
      end
      test_results = ftl_service.get_execution_steps(gcp_project, history_id, execution_id)
      tests_successful = extract_test_results(test_results, gcp_project, history_id, execution_id)
      unless executions_completed && tests_successful
        UI.test_failure!("Tests failed. " \
          "Go to #{firebase_console_link} for more information about this run")
      end
      return
    end

    # We should have caught all known states here. If the state is not one of them, this
    # plugin should be modified to handle that
    unless RUNNING_STATES.include?(state)
      spinner.error("Failed")
      UI.abort_with_message!("The test execution is in an unknown state: #{state}. " \
        "We appreciate if you could notify us at " \
        "https://github.com/fastlane/fastlane-plugin-firebase_test_lab/issues")
    end
    sleep(PULL_RESULT_INTERVAL)
  end
end