Files
gitlab-foss/scripts/failed_tests.rb
2025-07-11 12:09:48 +00:00

201 lines
5.8 KiB
Ruby
Executable File

#!/usr/bin/env ruby
# frozen_string_literal: true
require 'optparse'
require 'fileutils'
require 'uri'
require 'json'
class FailedTests
DEFAULT_OPTIONS = {
previous_tests_report_path: 'test_results/previous/test_reports.json',
output_directory: 'tmp/previous_failed_tests/',
format: :oneline,
rspec_pg_regex: /rspec .+ pg16( .+)?/,
rspec_ee_pg_regex: /rspec-ee .+ pg16( .+)?/,
jest_regex: /jest/,
rspec_regex: /rspec/
}.freeze
def self.parse_cli_options(args = ARGV)
options = FailedTests::DEFAULT_OPTIONS.dup
parser = OptionParser.new do |opts|
opts.on("-p", "--previous-tests-report-path PREVIOUS_TESTS_REPORT_PATH", String,
"Path of the file listing previous test failures (defaults to " \
"`#{FailedTests::DEFAULT_OPTIONS[:previous_tests_report_path]}`)") do |value|
options[:previous_tests_report_path] = value
end
opts.on("-o", "--output-directory OUTPUT_DIRECTORY", String,
"Output directory for failed test files (defaults to " \
"`#{FailedTests::DEFAULT_OPTIONS[:output_directory]}`)") do |value|
options[:output_directory] = value
end
opts.on("-f", "--format [oneline|json]", String,
"Format of the output files: oneline (with test filenames) or JSON (defaults to " \
"`#{FailedTests::DEFAULT_OPTIONS[:format]}`)") do |value|
options[:format] = value
end
opts.on("--rspec-pg-regex RSPEC_PG_REGEX", Regexp,
"Regex to use when finding matching RSpec jobs (defaults to " \
"`#{FailedTests::DEFAULT_OPTIONS[:rspec_pg_regex]}`)") do |value|
options[:rspec_pg_regex] = value
end
opts.on("--rspec-ee-pg-regex RSPEC_EE_PG_REGEX", Regexp,
"Regex to use when finding matching RSpec EE jobs (defaults to " \
"`#{FailedTests::DEFAULT_OPTIONS[:rspec_ee_pg_regex]}`)") do |value|
options[:rspec_ee_pg_regex] = value
end
opts.on("-h", "--help", "Prints this help") do
puts opts
exit
end
end
parser.parse!(args)
options
end
def initialize(options)
@filename = options.delete(:previous_tests_report_path)
@output_directory = options.delete(:output_directory)
@format = options.delete(:format).to_sym
@rspec_pg_regex = options.delete(:rspec_pg_regex)
@rspec_ee_pg_regex = options.delete(:rspec_ee_pg_regex)
end
def output_all_failed_rspec_tests
create_output_dir
all_failed_tests = []
rspec_failed_suites = failed_suites.select do |suite|
suite['name'].match?(FailedTests::DEFAULT_OPTIONS[:rspec_regex])
end
rspec_failed_suites.each do |suite|
failed_cases(suite).each do |test_case|
all_failed_tests << test_case
end
end
puts "[FailedTests] Detected #{all_failed_tests.size} total RSpec failed tests across all suites..."
write_failed_tests_to_file("rspec_all_failed_tests", all_failed_tests) if all_failed_tests.any?
puts "[FailedTests] All RSpec failed tests written to #{File.join(output_directory,
"rspec_all_failed_tests.#{output_file_format}")}"
end
def output_failed_tests
create_output_dir
output_all_failed_rspec_tests
failed_cases_for_suite_collection.each do |suite_name, suite_tests|
puts "[FailedTests] Detected #{suite_tests.size} failed tests in suite #{suite_name}..."
write_failed_tests_to_file("#{suite_name}_failed_tests", suite_tests)
end
end
def write_failed_tests_to_file(filename_prefix, failed_tests)
formatted_tests =
case format
when :oneline
# Deduplicate file paths for oneline format since we only output filenames
unique_files = failed_tests.map { |test| test['file'] }.uniq # rubocop:disable Rails/Pluck
unique_files.join(' ')
when :json
# Preserve all test case objects for JSON format to maintain full context
JSON.pretty_generate(failed_tests.to_a)
end
output_file = File.join(output_directory, "#{filename_prefix}.#{output_file_format}")
File.write(output_file, formatted_tests)
end
def failed_cases_for_suite_collection
suite_map.each_with_object(Hash.new { |h, k| h[k] = Set.new }) do |(suite_name, suite_collection_regex), hash|
failed_suites.each do |suite|
hash[suite_name].merge(failed_cases(suite)) if suite_collection_regex.match?(suite['name'])
end
end
end
def suite_map
@suite_map ||= {
rspec: rspec_pg_regex,
rspec_ee: rspec_ee_pg_regex,
jest: FailedTests::DEFAULT_OPTIONS[:jest_regex]
}
end
private
attr_reader :filename, :output_directory, :format, :rspec_pg_regex, :rspec_ee_pg_regex
def file_contents
@file_contents ||= begin
File.read(filename)
rescue Errno::ENOENT
'{}'
end
end
def file_contents_as_json
@file_contents_as_json ||= begin
JSON.parse(file_contents)
rescue JSON::ParserError
{}
end
end
def output_file_format
case format
when :oneline
'txt'
when :json
'json'
else
raise "[FailedTests] Unsupported format `#{format}` (allowed formats: `oneline` and `json`)!"
end
end
def failed_suites
return [] unless file_contents_as_json['suites']
file_contents_as_json['suites'].select { |suite| suite['failed_count'] > 0 }
end
def failed_cases(suite)
return [] unless suite
suite['test_cases'].filter_map do |failure_hash|
next if failure_hash['status'] != 'failed'
failure_hash['job_url'] = suite['job_url']
failure_hash['file'] = failure_hash['file'].delete_prefix('./')
failure_hash
end
end
def create_output_dir
return if File.directory?(output_directory)
puts '[FailedTests] Creating output directory...'
FileUtils.mkdir_p(output_directory)
end
end
if $PROGRAM_NAME == __FILE__
options = FailedTests.parse_cli_options
FailedTests.new(options).output_failed_tests
end