summaryrefslogtreecommitdiff
path: root/app/services/ci/test_failure_history_service.rb
blob: 99a2592ec06511481ce61389e8d310eb33e259a7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# 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?
      return false unless Feature.enabled?(:test_failure_history, project)
      return false unless project.default_branch_or_master == pipeline.ref

      # 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
      # 1 failed test case, then we have at least 201 failed test cases which exceeds
      # 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
      failed_test_cases = gather_failed_test_cases(failed_builds)

      return if failed_test_cases.size > MAX_TRACKABLE_FAILURES

      failed_test_cases.keys.each_slice(100) do |key_hashes|
        Ci::TestCase.transaction do
          ci_test_cases = Ci::TestCase.find_or_create_by_batch(project, key_hashes)
          failures = test_case_failures(ci_test_cases, failed_test_cases)

          Ci::TestCaseFailure.insert_all(failures)
        end
      end
    end

    def gather_failed_test_cases(failed_builds)
      failed_builds.each_with_object({}) do |build, failed_test_cases|
        test_suite = generate_test_suite!(build)
        test_suite.failed.keys.each do |key|
          failed_test_cases[key] = build
        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

    def test_case_failures(ci_test_cases, failed_test_cases)
      ci_test_cases.map do |test_case|
        build = failed_test_cases[test_case.key_hash]

        {
          test_case_id: test_case.id,
          build_id: build.id,
          failed_at: build.finished_at
        }
      end
    end
  end
end