Class: TestRun

Inherits:
ActiveRecord::Base
  • Object
show all
Includes:
BelongsToCommit
Defined in:
app/models/test_run.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.completedObject



39
40
41
# File 'app/models/test_run.rb', line 39

def completed
  where arel_table[:completed_at].not_eq(nil)
end

.erroredObject



51
52
53
# File 'app/models/test_run.rb', line 51

def errored
  where(result: "error")
end

.excluding(*test_runs_or_ids) ⇒ Object



34
35
36
37
# File 'app/models/test_run.rb', line 34

def excluding(*test_runs_or_ids)
  ids = test_runs_or_ids.flatten.map { |test_run_or_id| test_run_or_id.respond_to?(:id) ? test_run_or_id.id : test_run_or_id }
  where arel_table[:id].not_in(ids)
end

.failedObject



47
48
49
# File 'app/models/test_run.rb', line 47

def failed
  where(result: "fail")
end

.find_by_sha(sha) ⇒ Object



29
30
31
32
# File 'app/models/test_run.rb', line 29

def find_by_sha(sha)
  return nil if sha.blank?
  where(["sha LIKE ?", "#{sha}%"]).limit(1).first
end

.most_recentObject



55
56
57
58
59
60
61
62
63
64
65
# File 'app/models/test_run.rb', line 55

def most_recent
  joins "    INNER JOIN (\n      SELECT project_id, MAX(completed_at) AS completed_at\n      FROM test_runs\n      GROUP BY project_id\n    ) AS most_recent_test_runs\n    ON test_runs.project_id=most_recent_test_runs.project_id\n    AND test_runs.completed_at=most_recent_test_runs.completed_at\n  SQL\nend\n"

.passedObject



43
44
45
# File 'app/models/test_run.rb', line 43

def passed
  where(result: "pass")
end

Instance Method Details

#aborted?Boolean

Returns:

  • (Boolean)


82
83
84
# File 'app/models/test_run.rb', line 82

def aborted?
  result.to_s == "aborted"
end

#broken?Boolean

Returns:

  • (Boolean)


90
91
92
93
94
95
96
97
# File 'app/models/test_run.rb', line 90

def broken?
  return false unless failed_or_errored?

  last_tested_ancestor = commits_since_last_test_run.last
  return false if last_tested_ancestor.nil?

  project.test_runs.find_by_sha(last_tested_ancestor.sha).passed?
end

#commits_since_last_passing_test_runObject Also known as: blamable_commits



151
152
153
154
155
156
157
# File 'app/models/test_run.rb', line 151

def commits_since_last_passing_test_run
  shas_of_passing_commits = project.test_runs.passed.pluck(:sha)
  project.repo.ancestors_until(sha, including_self: true) { |ancestor|
    shas_of_passing_commits.member?(ancestor.sha) }
rescue Houston::Adapters::VersionControl::CommitNotFound
  []
end

#commits_since_last_test_runObject



143
144
145
146
147
148
149
# File 'app/models/test_run.rb', line 143

def commits_since_last_test_run
  shas_of_tested_commits = project.test_runs.excluding(self).pluck(:sha)
  project.repo.ancestors_until(sha, including_self: true) { |ancestor|
    shas_of_tested_commits.member?(ancestor.sha) }
rescue Houston::Adapters::VersionControl::CommitNotFound
  []
end

#compare_results!Object



310
311
312
313
314
315
# File 'app/models/test_run.rb', line 310

def compare_results!
  return if completed_without_running_tests?
  return unless commit
  compare_to_parent!
  compare_to_children!
end

#compare_to_children!Object



349
350
351
352
353
# File 'app/models/test_run.rb', line 349

def compare_to_children!
  commit.children.each do |commit|
    commit.test_run.compare_results! if commit.test_run
  end
end

#compare_to_parent!Object



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# File 'app/models/test_run.rb', line 317

def compare_to_parent!
  return if compared?
  return unless parent = commit.parent

  if parent.test_run
    if parent.test_run.completed?
      # Compare this Test Run with its parent
      # to see what changed in this one.
      TestRunComparer.compare!(parent.test_run, self)
      parent.test_run.compare_results!
    else
      # Wait for parent.test_run to complete
      # it'll run `compare_results!` then.
      # For now, do nothing.
    end
  else
    if passed?
      # Stop looking for answers. This Test Run
      # is the new baseline: when the whole suite
      # was building on Jenkins. (We don't need to
      # recurse all the way back to the project's
      # first commit!)
    else
      # This Test Run is failing and we don't
      # know whether the bug was introduced in this
      # commit or one of its ancestors. Have the
      # CI Server start with the first ancestor.
      parent.create_test_run!
    end
  end
end

#completed!(results_url) ⇒ Object



193
194
195
196
197
198
199
200
201
202
203
# File 'app/models/test_run.rb', line 193

def completed!(results_url)
  self.completed_at = Time.now unless completed?
  self.results_url = results_url
  save!
  fetch_results!

  if has_results?
    compare_results!
    fire_complete!
  end
end

#completed?Boolean

Returns:

  • (Boolean)


180
181
182
# File 'app/models/test_run.rb', line 180

def completed?
  completed_at.present?
end

#completed_without_running_tests?Boolean

Returns:

  • (Boolean)


86
87
88
# File 'app/models/test_run.rb', line 86

def completed_without_running_tests?
  %w{aborted error}.member?(result.to_s)
end

#coverage_detailObject



134
135
136
137
138
139
# File 'app/models/test_run.rb', line 134

def coverage_detail
  @coverage_detail ||= (Array(coverage).map do |file|
    file = file.with_indifferent_access
    SourceFileCoverage.new(project, sha, file[:filename], file[:coverage])
  end).select { |file| file.src.any? }
end

#create_tests_and_results(tests) ⇒ Object



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'app/models/test_run.rb', line 252

def create_tests_and_results(tests)
  tests = Array(tests)
  return if tests.empty?

  tests_map = Hash.new do |hash, (suite, name)|
    begin
      test = Test.create!(suite: suite, name: name, project_id: project_id)
    rescue ActiveRecord::RecordNotUnique
      test = Tests.find_by(suite: suite, name: name, project_id: project_id)
    end
    hash[[suite, name]] = test.id
  end

  Test.where(project_id: project_id).pluck(:suite, :name, :id).each do |suite, name, id|
    tests_map[[suite, name]] = id
  end

  errors_map = Hash[TestError.pluck(:sha, :id)]

  test_results = Houston.benchmark("Processing #{tests.count} test results") do
    tests.map do |test_attributes|
      suite = test_attributes.fetch :suite
      name = test_attributes.fetch :name

      status = test_attributes.fetch :status
      status = :fail if status == :error or status == :regression

      error_message = test_attributes[:error_message]
      error_backtrace = (test_attributes[:error_backtrace] || []).join("\n")
      output = [error_message, error_backtrace].reject(&:blank?).join("\n\n")
      if output.blank?
        error_id = nil
      else
        sha = Digest::SHA1.hexdigest(output)
        error_id = errors_map[sha]
        unless error_id
         error = TestError.create!(output: output)
         error_id = errors_map[error.sha] = error.id
        end
      end

      { test_run_id: id,
        test_id: tests_map[[suite, name]],
        error_id: output.blank? ? nil : output,
        status: status,
        error_id: error_id,
        duration: test_attributes.fetch(:duration, nil) }
    end.uniq { |attributes| attributes[:test_id] }
  end

  TestResult.where(test_run_id: id).delete_all
  Houston.benchmark("Inserting #{test_results.count} test results") do
    TestResult.insert_many(test_results)
  end
end

#failed?Boolean

Returns:

  • (Boolean)


70
71
72
# File 'app/models/test_run.rb', line 70

def failed?
  result.to_s == "fail"
end

#failed_or_errored?Boolean

Returns:

  • (Boolean)


74
75
76
# File 'app/models/test_run.rb', line 74

def failed_or_errored?
  %w{fail error}.member?(result.to_s)
end

#fetch_results!Object



205
206
207
208
209
210
# File 'app/models/test_run.rb', line 205

def fetch_results!
  update_attributes! project.ci_server.fetch_results!(results_url)
rescue Houston::Adapters::CIServer::Error
  update_column :result, "error"
  Rails.logger.error "#{$!.message}\n  #{$!.backtrace.join("\n  ")}"
end

#fire_complete!Object



212
213
214
# File 'app/models/test_run.rb', line 212

def fire_complete!
  Houston.observer.fire "test_run:complete", self
end

#fixed?Boolean

Returns:

  • (Boolean)


99
100
101
102
103
104
105
106
# File 'app/models/test_run.rb', line 99

def fixed?
  return false unless passed?

  last_tested_ancestor = commits_since_last_test_run.last
  return false if last_tested_ancestor.nil?

  project.test_runs.find_by_sha(last_tested_ancestor.sha).failed_or_errored?
end

#has_results?Boolean

Returns:

  • (Boolean)


184
185
186
# File 'app/models/test_run.rb', line 184

def has_results?
  result.present? and !aborted?
end

#identify_userObject



243
244
245
246
# File 'app/models/test_run.rb', line 243

def identify_user
  email = Mail::Address.new(agent_email)
  self.user = User.find_by_email_address(email.address)
end

#passed?Boolean

Returns:

  • (Boolean)


78
79
80
# File 'app/models/test_run.rb', line 78

def passed?
  result.to_s == "pass"
end

#real_fail_countObject



237
238
239
# File 'app/models/test_run.rb', line 237

def real_fail_count
  fail_count + regression_count
end

#retry!Object



162
163
164
# File 'app/models/test_run.rb', line 162

def retry!
  trigger_build!
end

#save_tests_and_resultsObject



248
249
250
# File 'app/models/test_run.rb', line 248

def save_tests_and_results
  create_tests_and_results read_attribute(:tests)
end

#short_commitObject



176
177
178
# File 'app/models/test_run.rb', line 176

def short_commit
  sha[0...7]
end

#short_description(with_duration: false) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'app/models/test_run.rb', line 108

def short_description(with_duration: false)
  passes = "#{pass_count} #{pass_count == 1 ? "test" : "tests"} passed"
  fails = "#{fail_count} #{fail_count == 1 ? "test" : "tests"} failed"
  duration = " in #{(self.duration / 1000.0).round(1)} seconds" if self.duration && with_duration

  if !completed?
    "#{project.ci_server_name} is running the tests"
  elsif passed?
    "#{passes}#{duration}"
  elsif failed?
    "#{passes} and #{fails}#{duration}"
  elsif aborted?
    "The test run was canceled"
  else
    "#{project.ci_server_name} was not able to run the tests"
  end
end

#start!Object



166
167
168
169
170
171
172
173
174
# File 'app/models/test_run.rb', line 166

def start!
  # Let's _not_ trigger the build if this Test Run is not going
  # to be saved. Let's also run BelongsToCommit#identify_commit
  # outside of the transaction that save! wraps it in — so that
  # we can recover from race conditions when creating commits.
  validate!
  trigger_build!
  save!
end

#testsObject



223
224
225
226
227
228
229
230
231
232
233
# File 'app/models/test_run.rb', line 223

def tests
  @tests ||= test_results.includes(:error).joins(:test).select("test_results.*", "tests.suite", "tests.name").map do |test_result|
    message, backtrace = test_result.error.output.split("\n\n") if test_result.error
    { suite: test_result[:suite],
      name: test_result[:name],
      status: test_result.status,
      duration: test_result.duration,
      error_message: message,
      error_backtrace: backtrace.to_s.split("\n") }
  end
end

#tests=(value) ⇒ Object



218
219
220
221
# File 'app/models/test_run.rb', line 218

def tests=(value)
  @tests = nil
  write_attribute :tests, value
end

#trigger_build!Object



188
189
190
191
# File 'app/models/test_run.rb', line 188

def trigger_build!
  project.ci_server.build!(sha)
  Houston.observer.fire "test_run:start", self
end

#urlObject



128
129
130
# File 'app/models/test_run.rb', line 128

def url
  "https://#{Houston.config.host}/projects/#{project.slug}/test_runs/#{sha}"
end