Files
openproject/script/support/github_actions_failures.rb
T
2026-03-10 15:26:38 -03:00

173 lines
5.4 KiB
Ruby

# frozen_string_literal: true
require "json"
module GithubActionsFailures
Result = Data.define(:errors, :failures_explanation, :merge_branch_sha)
class Error
attr_accessor :location, :page_html, :page_screenshot, :tests_group, :loading_error
end
class TestsGroup
attr_accessor :test_env_number, :seed, :files
def initialize
@files = []
end
def include_error?(error)
return false if error.location.nil?
files.any? { |file| error.location.include?(file) }
end
def inspect
"#<#{self.class} @test_env_number=#{test_env_number} @seed=#{seed} (#{files.count} files)>"
end
end
class JobErrorsFinder
SPEC_FAILURES_PATTERN = %r{^\S+ rspec (\S+) #.+$}
SPEC_LOADING_ERRORS_PATTERN = %r{^\S+ An error occurred while loading (\S+)\.\r?$}
SCREENSHOT_PATTERN = /\{"message":"Screenshot captured for failed feature test"[^\n]+$/
# Looks like this in the job log:
# rubocop:disable Layout/LineLength
# Process 28: TEST_ENV_NUMBER=28 RUBYOPT=-I/usr/local/bundle/bundler/gems/turbo_tests-3148ae6c3482/lib -r/usr/local/bundle/gems/bundler-2.5.23/lib/bundler/setup -W0 RSPEC_SILENCE_FILTER_ANNOUNCEMENTS=1 /usr/local/bundle/gems/bundler-2.5.23/exe/bundle exec rspec --seed 52674 --format TurboTests::JsonRowsFormatter --out tmp/test-pipes/subprocess-28 --format ParallelTests::RSpec::RuntimeLogger --out spec/support/turbo_runtime_features.log spec/features/api_docs/index_spec.rb spec/features/custom_fields/reorder_options_spec.rb spec/features/projects/projects_portfolio_spec.rb spec/features/projects/template_spec.rb spec/features/versions/edit_spec.rb spec/features/work_packages/details/markdown/description_editor_spec.rb spec/features/work_packages/table/hierarchy/hierarchy_parent_below_spec.rb spec/features/work_packages/table/inline_create/inline_create_refresh_spec.rb spec/features/work_packages/table/invalid_query_spec.rb spec/features/work_packages/tabs/activity_revisions_spec.rb
# rubocop:enable Layout/LineLength
TESTS_GROUP_PATTERN = /Process \d+: TEST_ENV_NUMBER=\d+ [^\n]+$/
BRANCH_MERGE_PATTERN = /Merge \w{40} into (\w{40})$/
def self.scan_logs(logs)
finder = new
logs.each do |log|
finder.scan_log(log)
end
Result.new(
errors: finder.errors,
failures_explanation: finder.failures_explanation,
merge_branch_sha: finder.merge_branch_sha
)
end
attr_reader :failures_explanation, :merge_branch_sha
def scan_log(log)
find_failures(log)
find_failures_explanation(log)
find_loading_errors(log)
find_screenshots(log)
find_tests_groups(log)
find_merge_branch_info(log)
end
def errors
@errors.values
end
private
def initialize
@errors = {}
end
def create_error(location)
return if location.nil?
error = Error.new
error.location = location
@errors[location] ||= error
end
def with_matching_error(location: nil, id: nil)
error = @errors[id] || @errors[location]
yield error if error && block_given?
error
end
def find_failures(log)
log.scan(SPEC_FAILURES_PATTERN)
.flatten
.uniq
.sort
.each do |rerun_location|
create_error(rerun_location)
end
end
def find_failures_explanation(log)
explanations = []
log.split("\n").each do |line|
if line.end_with?("Failures:") .. line.end_with?("Failed examples:")
explanations << line
end
end
explanations.map! { it[29..] } # Remove leading GitHub Actions log timestamp (e.g. "2024-02-05T08:37:54.5175930Z ")
explanations.reject! do |line|
line == "Failures:" ||
line == "Failed examples:" ||
line.include?("gems/rspec-retry-") ||
line.include?("gems/webmock-")
end
@failures_explanation = explanations.join("\n")
end
def find_loading_errors(log)
log.scan(SPEC_LOADING_ERRORS_PATTERN)
.flatten
.uniq
.sort
.each do |location|
error = create_error(location)
error.loading_error = true
end
end
def find_screenshots(log)
log.scan(SCREENSHOT_PATTERN)
.map { JSON.parse(it) }
.each do |screenshot_info|
id = screenshot_info["test_id"]
location = screenshot_info["test_location"]
with_matching_error(location:, id:) do |error|
error.page_html = screenshot_info["html"]
error.page_screenshot = screenshot_info["image"]
end
end
end
def find_tests_groups(log)
tests_groups = log
.scan(TESTS_GROUP_PATTERN)
.flatten
.map { build_tests_group_from_command(it) }
errors.each do |error|
error.tests_group = tests_groups.find { it.include_error?(error) }
end
end
def find_merge_branch_info(log)
merge_branch_sha = log.scan(BRANCH_MERGE_PATTERN).flatten.first
@merge_branch_sha = merge_branch_sha if merge_branch_sha
end
def build_tests_group_from_command(line)
tests_group = TestsGroup.new
parts = line.split
while parts.any?
case part = parts.shift
when /^TEST_ENV_NUMBER=/
tests_group.test_env_number = part.delete_prefix("TEST_ENV_NUMBER=")
when "--seed"
tests_group.seed = parts.shift
when /_spec.rb$/
tests_group.files << part
end
end
tests_group
end
end
end