Class: Mergify::RSpec::FlakyDetector

Inherits:
Object
  • Object
show all
Defined in:
lib/mergify/rspec/flaky_detection.rb

Overview

Manages intelligent test rerunning with budget constraints for flaky detection. rubocop:disable Metrics/ClassLength

Defined Under Namespace

Classes: TestMetrics

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(token:, url:, full_repository_name:, mode:) ⇒ FlakyDetector

Returns a new instance of FlakyDetector.



61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/mergify/rspec/flaky_detection.rb', line 61

def initialize(token:, url:, full_repository_name:, mode:)
  @token = token
  @url = url
  @full_repository_name = full_repository_name
  @mode = mode
  @metrics = {}
  @over_length_tests = Set.new
  @tests_to_process = []
  @budget = 0.0

  fetch_context
  validate!
end

Instance Attribute Details

#budgetObject (readonly)

Returns the value of attribute budget.



59
60
61
# File 'lib/mergify/rspec/flaky_detection.rb', line 59

def budget
  @budget
end

#modeObject (readonly)

Returns the value of attribute mode.



59
60
61
# File 'lib/mergify/rspec/flaky_detection.rb', line 59

def mode
  @mode
end

#tests_to_processObject (readonly)

Returns the value of attribute tests_to_process.



59
60
61
# File 'lib/mergify/rspec/flaky_detection.rb', line 59

def tests_to_process
  @tests_to_process
end

Instance Method Details

#fill_metrics_from_report(test_id, phase, duration, status) ⇒ Object

rubocop:disable Metrics/MethodLength



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/mergify/rspec/flaky_detection.rb', line 103

def fill_metrics_from_report(test_id, phase, duration, status)
  if status == :skipped
    @metrics.delete(test_id)
    return
  end

  return unless @tests_to_process.include?(test_id)

  if test_id.length > @context[:max_test_name_length]
    @over_length_tests.add(test_id)
    return
  end

  # Only initialize metrics when the first phase is "setup"
  return if !@metrics.key?(test_id) && phase != 'setup'

  @metrics[test_id] ||= TestMetrics.new
  @metrics[test_id].fill_from_report(phase, duration, status)
end

#last_rerun_for_test?(test_id) ⇒ Boolean

Returns:

  • (Boolean)


156
157
158
159
160
161
# File 'lib/mergify/rspec/flaky_detection.rb', line 156

def last_rerun_for_test?(test_id)
  return false unless @metrics.key?(test_id)

  metrics = @metrics[test_id]
  metrics.will_exceed_deadline? || metrics.rerun_count >= @context[:max_test_execution_count]
end

#make_reportObject

rubocop:disable Metrics/MethodLength,Metrics/AbcSize



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/mergify/rspec/flaky_detection.rb', line 168

def make_report
  lines = []
  lines << 'Mergify Flaky Detection Report'
  lines << "  Mode        : #{@mode}"
  lines << "  Budget      : #{format('%.2f', @budget)}s"
  lines << "  Budget used : #{format('%.2f', budget_used)}s"
  lines << "  Tests tracked: #{@metrics.size}"
  lines << ''

  @metrics.each do |test_id, m|
    lines << "  #{test_id}"
    lines << "    Reruns       : #{m.rerun_count}"
    lines << "    Initial dur  : #{format('%.3f', m.initial_duration)}s"
    lines << "    Total dur    : #{format('%.3f', m.total_duration)}s"
    lines << "    Timeout warn : #{m.prevented_timeout}" if m.prevented_timeout
  end

  lines << '' unless @over_length_tests.empty?
  @over_length_tests.each do |id|
    lines << "  WARNING: test name too long (skipped): #{id[0, 80]}..."
  end

  lines.join("\n")
end

#prepare_for_session(test_ids) ⇒ Object

rubocop:disable Metrics/MethodLength,Metrics/AbcSize



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/mergify/rspec/flaky_detection.rb', line 76

def prepare_for_session(test_ids)
  existing = Set.new(@context[:existing_test_names])
  unhealthy = Set.new(@context[:unhealthy_test_names])

  @tests_to_process =
    if @mode == 'new'
      test_ids.reject { |id| existing.include?(id) }
    else
      test_ids.select { |id| unhealthy.include?(id) }
    end

  budget_ratio = if @mode == 'new'
                   @context[:budget_ratio_for_new_tests]
                 else
                   @context[:budget_ratio_for_unhealthy_tests]
                 end

  mean_duration_s = @context[:existing_tests_mean_duration_ms] / 1000.0
  existing_count = @context[:existing_test_names].size
  min_budget_s = @context[:min_budget_duration_ms] / 1000.0

  ratio_budget = budget_ratio * mean_duration_s * existing_count
  @budget = [ratio_budget, min_budget_s].max
end

#rerunning_test?(test_id) ⇒ Boolean

rubocop:enable Metrics/MethodLength

Returns:

  • (Boolean)


124
125
126
# File 'lib/mergify/rspec/flaky_detection.rb', line 124

def rerunning_test?(test_id)
  @metrics.key?(test_id) && @metrics[test_id].rerun_count >= 1
end

#set_test_deadline(test_id, timeout: nil) ⇒ Object



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/mergify/rspec/flaky_detection.rb', line 132

def set_test_deadline(test_id, timeout: nil)
  return unless @metrics.key?(test_id)

  remaining_tests = [remaining_tests_count, 1].max
  per_test_budget = remaining_budget / remaining_tests

  allocated =
    if timeout
      [per_test_budget, timeout * 0.9].min
    else
      per_test_budget
    end

  @metrics[test_id].deadline = Time.now.to_f + allocated
end

#test_metrics(test_id) ⇒ Object



163
164
165
# File 'lib/mergify/rspec/flaky_detection.rb', line 163

def test_metrics(test_id)
  @metrics[test_id]
end

#test_rerun?(test_id) ⇒ Boolean

Returns:

  • (Boolean)


128
129
130
# File 'lib/mergify/rspec/flaky_detection.rb', line 128

def test_rerun?(test_id)
  @metrics.key?(test_id) && @metrics[test_id].rerun_count > 1
end

#test_too_slow?(test_id) ⇒ Boolean

Returns:

  • (Boolean)


148
149
150
151
152
153
154
# File 'lib/mergify/rspec/flaky_detection.rb', line 148

def test_too_slow?(test_id)
  return false unless @metrics.key?(test_id)

  metrics = @metrics[test_id]
  min_exec = @context[:min_test_execution_count]
  (metrics.initial_duration * min_exec) > metrics.remaining_time
end