File: tests-metadata.rb

package info (click to toggle)
gitlab 17.6.5-19
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 629,368 kB
  • sloc: ruby: 1,915,304; javascript: 557,307; sql: 60,639; xml: 6,509; sh: 4,567; makefile: 1,239; python: 406
file content (191 lines) | stat: -rwxr-xr-x 5,068 bytes parent folder | download
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
#!/usr/bin/env ruby
# frozen_string_literal: true

require 'fileutils'
require 'json'

class TestsMetadata < Struct.new( # rubocop:disable Style/StructInheritance -- Otherwise we cannot define a nested constant
  :mode,
  :knapsack_report_path, :flaky_report_path, :fast_quarantine_path,
  :average_knapsack,
  keyword_init: true)

  FALLBACK_JSON = '{}'

  def main
    abort("Unknown mode: `#{mode}`. It must be `retrieve` or `update`.") unless
      mode == 'retrieve' || mode == 'update' || mode == 'verify'

    if mode == 'verify'
      verify
    else
      prepare_directories
      retrieve
      update if mode == 'update'
    end
  end

  private

  def verify
    verify_knapsack_report
    verify_flaky_report
    verify_fast_quarantine
    puts 'OK'
  end

  def verify_knapsack_report
    report = JSON.parse(File.read(knapsack_report_path))

    valid = report.is_a?(Hash) &&
      report.all? do |spec, duration|
        spec.is_a?(String) && duration.is_a?(Numeric)
      end

    valid || abort("#{knapsack_report_path} is not a valid Knapsack report")
  rescue JSON::ParserError
    abort("#{knapsack_report_path} is not valid JSON")
  end

  def verify_flaky_report
    # This requires activesupport
    require_relative '../../gems/gitlab-rspec_flaky/lib/gitlab/rspec_flaky/report'

    Gitlab::RspecFlaky::Report.load(flaky_report_path).flaky_examples.to_h
  rescue JSON::ParserError
    abort("#{flaky_report_path} is not valid JSON")
  end

  def verify_fast_quarantine
    require_relative '../../tooling/lib/tooling/fast_quarantine'

    fast_quarantine =
      Tooling::FastQuarantine.new(fast_quarantine_path: fast_quarantine_path)

    fast_quarantine.identifiers
  end

  def prepare_directories
    FileUtils.mkdir_p([
      File.dirname(knapsack_report_path),
      File.dirname(flaky_report_path),
      File.dirname(fast_quarantine_path)
    ])
  end

  def retrieve
    tasks = []

    tasks << async_curl_download_json(
      url: "https://gitlab-org.gitlab.io/gitlab/#{knapsack_report_path}",
      path: knapsack_report_path,
      fallback_content: FALLBACK_JSON
    )

    tasks << async_curl_download_json(
      url: "https://gitlab-org.gitlab.io/gitlab/#{flaky_report_path}",
      path: flaky_report_path,
      fallback_content: FALLBACK_JSON
    )

    tasks << async_curl_download(
      url: "https://gitlab-org.gitlab.io/quality/engineering-productivity/fast-quarantine/#{fast_quarantine_path}",
      path: fast_quarantine_path,
      fallback_content: ''
    )

    tasks.compact.each(&:join)
  end

  def update
    update_knapsack_report
    update_flaky_report
    # Prune flaky tests that weren't flaky in the last 7 days, *after* updating the flaky tests detected
    # in this pipeline, so that first_flaky_at for tests that are still flaky is maintained.
    prune_flaky_report
  end

  def update_knapsack_report
    new_reports = Dir["#{File.dirname(knapsack_report_path)}/rspec*.json"]

    if average_knapsack
      system_abort_if_failed(%W[
        scripts/pipeline/average_reports.rb
        -i #{knapsack_report_path}
        -n #{new_reports.join(',')}
      ])
    else
      system_abort_if_failed(%W[
        scripts/merge-reports
        #{knapsack_report_path}
        #{new_reports.join(' ')}
      ])
    end
  end

  def update_flaky_report
    new_reports = Dir["#{File.dirname(flaky_report_path)}/all_*.json"]

    system_abort_if_failed(%W[
      scripts/merge-reports
      #{flaky_report_path}
      #{new_reports.join(' ')}
    ])
  end

  def prune_flaky_report
    system_abort_if_failed(%W[
      scripts/flaky_examples/prune-old-flaky-examples
      #{flaky_report_path}
    ])
  end

  def async_curl_download_json(**args)
    async_curl_download(**args) do |content|
      JSON.parse(content)
    rescue JSON::ParserError
      false
    end
  end

  def async_curl_download(url:, path:, fallback_content:)
    if force_download? || !File.exist?(path) # rubocop:disable Style/GuardClause -- This is easier to read
      async do
        success = system(*%W[curl --fail --location -o #{path} #{url}])

        if success
          if block_given? # rubocop:disable Style/IfUnlessModifier -- This is easier to read
            yield(File.read(path)) || File.write(path, fallback_content)
          end
        else
          File.write(path, fallback_content)
        end
      end
    end
  end

  def force_download?
    mode == 'retrieve'
  end

  def system_abort_if_failed(command)
    system(*command) || abort("Command failed for: #{command.join(' ')}")
  end

  def async(&task)
    Thread.new(&task)
  end
end

if $PROGRAM_NAME == __FILE__
  TestsMetadata.new(
    mode: ARGV.first,
    knapsack_report_path: ENV['KNAPSACK_RSPEC_SUITE_REPORT_PATH'] ||
      'knapsack/report-master.json',
    flaky_report_path: ENV['FLAKY_RSPEC_SUITE_REPORT_PATH'] ||
      'rspec/flaky/report-suite.json',
    fast_quarantine_path: ENV['RSPEC_FAST_QUARANTINE_PATH'] ||
      'rspec/fast_quarantine-gitlab.txt',
    average_knapsack: ENV['AVERAGE_KNAPSACK_REPORT'] == 'true'
  ).main
end