2020-12-10 10:10:12 -05:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
module Ci
|
|
|
|
class TestFailureHistoryService
|
|
|
|
class Async
|
|
|
|
attr_reader :service
|
|
|
|
|
|
|
|
def initialize(service)
|
|
|
|
@service = service
|
|
|
|
end
|
|
|
|
|
|
|
|
def perform_if_needed
|
|
|
|
TestFailureHistoryWorker.perform_async(service.pipeline.id) if service.should_track_failures?
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
MAX_TRACKABLE_FAILURES = 200
|
|
|
|
|
|
|
|
attr_reader :pipeline
|
|
|
|
delegate :project, to: :pipeline
|
|
|
|
|
|
|
|
def initialize(pipeline)
|
|
|
|
@pipeline = pipeline
|
|
|
|
end
|
|
|
|
|
|
|
|
def execute
|
|
|
|
return unless should_track_failures?
|
|
|
|
|
|
|
|
track_failures
|
|
|
|
end
|
|
|
|
|
|
|
|
def should_track_failures?
|
2021-05-04 08:10:04 -04:00
|
|
|
return false unless project.default_branch_or_main == pipeline.ref
|
2020-12-10 10:10:12 -05:00
|
|
|
|
|
|
|
# We fetch for up to MAX_TRACKABLE_FAILURES + 1 builds. So if ever we get
|
|
|
|
# 201 total number of builds with the assumption that each job has at least
|
2021-03-19 08:09:03 -04:00
|
|
|
# 1 failed unit test, then we have at least 201 failed unit tests which exceeds
|
2020-12-10 10:10:12 -05:00
|
|
|
# the MAX_TRACKABLE_FAILURES of 200. If this is the case, let's early exit so we
|
|
|
|
# don't have to parse each JUnit report of each of the 201 builds.
|
|
|
|
failed_builds.length <= MAX_TRACKABLE_FAILURES
|
|
|
|
end
|
|
|
|
|
|
|
|
def async
|
|
|
|
Async.new(self)
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def failed_builds
|
|
|
|
@failed_builds ||= pipeline.builds_with_failed_tests(limit: MAX_TRACKABLE_FAILURES + 1)
|
|
|
|
end
|
|
|
|
|
|
|
|
def track_failures
|
2021-03-19 08:09:03 -04:00
|
|
|
failed_unit_tests = gather_failed_unit_tests_from_reports(failed_builds)
|
2020-12-10 10:10:12 -05:00
|
|
|
|
2021-03-19 08:09:03 -04:00
|
|
|
return if failed_unit_tests.size > MAX_TRACKABLE_FAILURES
|
2020-12-10 10:10:12 -05:00
|
|
|
|
2021-03-19 08:09:03 -04:00
|
|
|
failed_unit_tests.each_slice(100) do |batch|
|
|
|
|
Ci::UnitTest.transaction do
|
|
|
|
unit_test_attrs = ci_unit_test_attrs(batch)
|
|
|
|
ci_unit_tests = Ci::UnitTest.find_or_create_by_batch(project, unit_test_attrs)
|
2020-12-10 10:10:12 -05:00
|
|
|
|
2021-03-19 08:09:03 -04:00
|
|
|
failures = ci_unit_test_failure_attrs(ci_unit_tests, failed_unit_tests)
|
|
|
|
Ci::UnitTestFailure.insert_all(failures)
|
2020-12-10 10:10:12 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-03-19 08:09:03 -04:00
|
|
|
def gather_failed_unit_tests_from_reports(failed_builds)
|
|
|
|
failed_builds.each_with_object({}) do |build, failed_unit_tests|
|
2020-12-10 10:10:12 -05:00
|
|
|
test_suite = generate_test_suite!(build)
|
2021-03-19 08:09:03 -04:00
|
|
|
test_suite.failed.each do |key, unit_test|
|
|
|
|
failed_unit_tests[key] = {
|
|
|
|
build: build, # This will be used in ci_unit_test_failure_attrs
|
|
|
|
unit_test: unit_test # This will be used in ci_unit_test_attrs
|
|
|
|
}
|
2020-12-10 10:10:12 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def generate_test_suite!(build)
|
|
|
|
# Returns an instance of Gitlab::Ci::Reports::TestSuite
|
|
|
|
build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
|
|
|
|
end
|
|
|
|
|
2021-03-19 08:09:03 -04:00
|
|
|
def ci_unit_test_attrs(batch)
|
|
|
|
batch.map do |item|
|
|
|
|
unit_test = item.last[:unit_test]
|
2020-12-10 10:10:12 -05:00
|
|
|
|
|
|
|
{
|
2021-03-19 08:09:03 -04:00
|
|
|
key_hash: unit_test.key,
|
|
|
|
name: unit_test.name,
|
|
|
|
suite_name: unit_test.suite_name
|
|
|
|
}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def ci_unit_test_failure_attrs(ci_unit_tests, failed_unit_tests)
|
|
|
|
ci_unit_tests.map do |ci_unit_test|
|
|
|
|
build = failed_unit_tests[ci_unit_test.key_hash][:build]
|
|
|
|
|
|
|
|
{
|
|
|
|
unit_test_id: ci_unit_test.id,
|
2020-12-10 10:10:12 -05:00
|
|
|
build_id: build.id,
|
|
|
|
failed_at: build.finished_at
|
|
|
|
}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|