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



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

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

.erroredObject



55
56
57
# File 'app/models/test_run.rb', line 55

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



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

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



59
60
61
62
63
64
65
66
67
68
69
# File 'app/models/test_run.rb', line 59

def most_recent
  joins <<-SQL
    INNER JOIN (
      SELECT project_id, MAX(completed_at) AS completed_at
      FROM test_runs
      GROUP BY project_id
    ) AS most_recent_test_runs
    ON test_runs.project_id=most_recent_test_runs.project_id
    AND test_runs.completed_at=most_recent_test_runs.completed_at
  SQL
end

.passedObject



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

def passed
  where(result: "pass")
end

.pendingObject



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

def pending
  where completed_at: nil
end

.rebuild_tests!(options = {}) ⇒ Object



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'app/models/test_run.rb', line 71

def rebuild_tests!(options={})
  test_runs = where("tests is not null")
             .where("id NOT IN (SELECT DISTINCT test_run_id FROM test_results)")
  if options[:progress]
    require "progressbar"
    pbar = ProgressBar.new("test runs", test_runs.count)
  end
  test_runs.find_each do |test_run|
    if test_run.read_attribute(:tests).nil?
      test_run.update_column :tests, nil
    else
      test_run.save_tests_and_results
    end
    pbar.inc if options[:progress]
  end
  pbar.finish if options[:progress]
end

Instance Method Details

#aborted?Boolean

Returns:

  • (Boolean)


104
105
106
# File 'app/models/test_run.rb', line 104

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

#broken?Boolean

Returns:

  • (Boolean)


112
113
114
115
116
117
118
119
# File 'app/models/test_run.rb', line 112

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



173
174
175
176
177
178
179
# File 'app/models/test_run.rb', line 173

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



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

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



337
338
339
340
341
342
# File 'app/models/test_run.rb', line 337

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

#compare_to_children!Object



376
377
378
379
380
# File 'app/models/test_run.rb', line 376

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

#compare_to_parent!Object



344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# File 'app/models/test_run.rb', line 344

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



219
220
221
222
223
224
225
226
227
228
229
# File 'app/models/test_run.rb', line 219

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)


202
203
204
# File 'app/models/test_run.rb', line 202

def completed?
  completed_at.present?
end

#completed_without_running_tests?Boolean

Returns:

  • (Boolean)


108
109
110
# File 'app/models/test_run.rb', line 108

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

#coverage_detailObject



156
157
158
159
160
161
# File 'app/models/test_run.rb', line 156

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



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
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'app/models/test_run.rb', line 279

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)


92
93
94
# File 'app/models/test_run.rb', line 92

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

#failed_or_errored?Boolean

Returns:

  • (Boolean)


96
97
98
# File 'app/models/test_run.rb', line 96

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

#fetch_results!Object



231
232
233
234
235
236
# File 'app/models/test_run.rb', line 231

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



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

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

#fixed?Boolean

Returns:

  • (Boolean)


121
122
123
124
125
126
127
128
# File 'app/models/test_run.rb', line 121

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)


210
211
212
# File 'app/models/test_run.rb', line 210

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

#identify_userObject



270
271
272
273
# File 'app/models/test_run.rb', line 270

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

#passed?Boolean

Returns:

  • (Boolean)


100
101
102
# File 'app/models/test_run.rb', line 100

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

#pending?Boolean

Returns:

  • (Boolean)


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

def pending?
  !completed?
end

#real_fail_countObject



264
265
266
# File 'app/models/test_run.rb', line 264

def real_fail_count
  fail_count + regression_count
end

#retry!Object



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

def retry!
  trigger_build!
end

#save_tests_and_resultsObject



275
276
277
# File 'app/models/test_run.rb', line 275

def save_tests_and_results
  create_tests_and_results read_attribute(:tests)
end

#short_commitObject



198
199
200
# File 'app/models/test_run.rb', line 198

def short_commit
  sha[0...7]
end

#short_description(with_duration: false) ⇒ Object



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'app/models/test_run.rb', line 130

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



188
189
190
191
192
193
194
195
196
# File 'app/models/test_run.rb', line 188

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



249
250
251
252
253
254
255
256
257
258
259
260
# File 'app/models/test_run.rb', line 249

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
    { test_id: test_result.test_id,
      suite: test_result[:suite],
      name: test_result[:name].to_s.gsub(/^(test :|: )/, ""),
      status: test_result.status,
      duration: test_result.duration,
      error_message: message,
      error_backtrace: backtrace.to_s.split("\n") }
  end
end

#tests=(value) ⇒ Object



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

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

#trigger_build!Object



214
215
216
217
# File 'app/models/test_run.rb', line 214

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

#urlObject



150
151
152
# File 'app/models/test_run.rb', line 150

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