Class: Fastlane::Actions::FirebaseTestLabIosXctestAction

Inherits:
Action
  • Object
show all
Defined in:
lib/fastlane/plugin/firebase_test_lab/actions/firebase_test_lab_ios_xctest.rb

Documentation collapse

Class Method Summary collapse

Class Method Details

.authorsObject



266
267
268
# File 'lib/fastlane/plugin/firebase_test_lab/actions/firebase_test_lab_ios_xctest.rb', line 266

def self.authors
  ["powerivq"]
end

.available_optionsObject



262
263
264
# 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

.descriptionObject



258
259
260
# 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) ⇒ Object



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# 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) ⇒ Object



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# 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_nameObject



163
164
165
166
# 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) ⇒ Boolean

Returns:

  • (Boolean)


270
271
272
# 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) ⇒ Object



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# 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) ⇒ Object



168
169
170
171
172
173
174
175
176
177
# 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) ⇒ Object



78
79
80
81
82
83
# 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) ⇒ Object



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# 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