#!/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