Reapply "Enable strict typing in Homebrew::TestBot"

This reverts commit 6bab83ccc9.
This commit is contained in:
Douglas Eichelberger
2026-02-07 09:10:28 -08:00
parent 730d145011
commit 1a956e7b77
18 changed files with 845 additions and 557 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
# frozen_string_literal: true
require "test_bot"
require "dev-cmd/test-bot"
RSpec.describe Homebrew::TestBot::Setup do
subject(:setup) { described_class.new }
@@ -11,7 +11,7 @@ RSpec.describe Homebrew::TestBot::Setup do
.exactly(3).times
.and_return(instance_double(Homebrew::TestBot::Step, passed?: true))
expect(setup.run!(args: instance_double(Homebrew::CLI::Args)).passed?).to be(true)
expect(setup.run!(args: instance_double(Homebrew::Cmd::TestBotCmd::Args)).passed?).to be(true)
end
end
end
+5 -1
View File
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "test_bot/step"
@@ -23,14 +23,17 @@ module Homebrew
HOMEBREW_TAP_REGEX = %r{^([\w-]+)/homebrew-([\w-]+)$}
sig { params(args: Homebrew::Cmd::TestBotCmd::Args).returns(T::Boolean) }
def cleanup?(args)
args.cleanup? || GitHub::Actions.env_set?
end
sig { params(args: Homebrew::Cmd::TestBotCmd::Args).returns(T::Boolean) }
def local?(args)
args.local? || GitHub::Actions.env_set?
end
sig { params(tap: T.nilable(String)).returns(T.nilable(Tap)) }
def resolve_test_tap(tap = nil)
return Tap.fetch(tap) if tap
@@ -52,6 +55,7 @@ module Homebrew
end
end
sig { params(args: Homebrew::Cmd::TestBotCmd::Args).void }
def run!(args)
$stdout.sync = true
$stderr.sync = true
@@ -4,8 +4,10 @@
module Homebrew
module TestBot
class BottlesFetch < TestFormulae
sig { returns(T::Array[String]) }
attr_accessor :testing_formulae
sig { params(args: Homebrew::Cmd::TestBotCmd::Args).void }
def run!(args:)
info_header "Testing formulae:"
puts testing_formulae
@@ -19,6 +21,7 @@ module Homebrew
private
sig { returns(T::Hash[Symbol, T::Set[String]]) }
def formulae_by_tag
tags = Hash.new { |hash, key| hash[key] = Set.new }
@@ -38,6 +41,7 @@ module Homebrew
tags
end
sig { params(tag: Symbol, formulae: T::Set[String], args: Homebrew::Cmd::TestBotCmd::Args).void }
def fetch_bottles!(tag, formulae, args:)
test_header(:BottlesFetch, method: "fetch_bottles!(#{tag})")
+3 -1
View File
@@ -1,9 +1,10 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
module Homebrew
module TestBot
class CleanupAfter < TestCleanup
sig { params(args: Homebrew::Cmd::TestBotCmd::Args).void }
def run!(args:)
if ENV["HOMEBREW_GITHUB_ACTIONS"].present? && ENV["GITHUB_ACTIONS_HOMEBREW_SELF_HOSTED"].blank? &&
# don't need to do post-build cleanup unless testing test-bot itself.
@@ -27,6 +28,7 @@ module Homebrew
private
sig { void }
def pkill_if_needed
pgrep = ["pgrep", "-f", HOMEBREW_CELLAR.to_s]
+2 -1
View File
@@ -1,9 +1,10 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
module Homebrew
module TestBot
class CleanupBefore < TestCleanup
sig { params(args: Homebrew::Cmd::TestBotCmd::Args).void }
def run!(args:)
test_header(:CleanupBefore)
+248 -206
View File
@@ -1,21 +1,48 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
module Homebrew
module TestBot
class Formulae < TestFormulae
attr_writer :testing_formulae, :added_formulae, :deleted_formulae
sig { params(testing_formulae: T::Array[String]).void }
attr_writer :testing_formulae
sig { params(added_formulae: T::Array[String]).void }
attr_writer :added_formulae
sig { params(deleted_formulae: T::Array[String]).void }
attr_writer :deleted_formulae
sig {
params(
tap: T.nilable(Tap),
git: String,
dry_run: T::Boolean,
fail_fast: T::Boolean,
verbose: T::Boolean,
output_paths: T::Hash[Symbol, Pathname],
).void
}
def initialize(tap:, git:, dry_run:, fail_fast:, verbose:, output_paths:)
super(tap:, git:, dry_run:, fail_fast:, verbose:)
@built_formulae = []
@bottle_checksums = {}
@bottle_output_path = output_paths[:bottle]
@linkage_output_path = output_paths[:linkage]
@skipped_or_failed_formulae_output_path = output_paths[:skipped_or_failed_formulae]
@built_formulae = T.let([], T::Array[String])
@bottle_checksums = T.let({}, T::Hash[Pathname, String])
@bottle_output_path = T.let(output_paths.fetch(:bottle), Pathname)
@linkage_output_path = T.let(output_paths.fetch(:linkage), Pathname)
@skipped_or_failed_formulae_output_path = T.let(output_paths.fetch(:skipped_or_failed_formulae), Pathname)
@testing_formulae = T.let([], T::Array[String])
@added_formulae = T.let([], T::Array[String])
@deleted_formulae = T.let([], T::Array[String])
@testing_formulae_count = T.let(0, Integer)
@tested_formulae_count = T.let(0, Integer)
@unchanged_dependencies = T.let([], T::Array[String])
@unchanged_build_dependencies = T.let([], T::Array[String])
@bottle_filename = T.let(nil, T.nilable(Pathname))
@bottle_json_filename = T.let(nil, T.nilable(Pathname))
end
sig { params(args: Homebrew::Cmd::TestBotCmd::Args).void }
def run!(args:)
test_header(:Formulae)
@@ -87,6 +114,7 @@ module Homebrew
private
sig { params(deps: T::Array[Dependency]).void }
def tap_needed_taps(deps)
deps.each { |d| d.to_formula.recursive_dependencies }
rescue TapFormulaUnavailableError => e
@@ -97,6 +125,7 @@ module Homebrew
retry
end
sig { void }
def install_ca_certificates_if_needed
return if DevelopmentTools.ca_file_handles_most_https_certificates?
@@ -104,6 +133,7 @@ module Homebrew
env: { "HOMEBREW_DEVELOPER" => nil }
end
sig { params(formula: Formula, formula_name: String, args: Homebrew::Cmd::TestBotCmd::Args).void }
def setup_formulae_deps_instances(formula, formula_name, args:)
conflicts = formula.conflicts
formula_recursive_dependencies = formula.recursive_dependencies.map(&:to_formula)
@@ -191,14 +221,16 @@ module Homebrew
@unchanged_build_dependencies = build_dependencies - @testing_formulae
end
sig { params(formula: Formula).void }
def cleanup_bottle_etc_var(formula)
# Restore etc/var files from bottle so dependents can use them.
formula.install_etc_var
end
sig { returns(T::Boolean) }
def verify_local_bottles
# Portable Ruby bottles are handled differently.
return if testing_portable_ruby?
return false if testing_portable_ruby?
# Setting `HOMEBREW_DISABLE_LOAD_FORMULA` probably doesn't do anything here but let's set it just to be safe.
with_env(HOMEBREW_DISABLE_LOAD_FORMULA: "1") do
@@ -245,17 +277,18 @@ module Homebrew
end
end
sig { params(formula: Formula, new_formula: T.nilable(T::Boolean), args: Homebrew::Cmd::TestBotCmd::Args).void }
def bottle_reinstall_formula(formula, new_formula, args:)
unless build_bottle?(formula, args:)
@bottle_filename = nil
@bottle_filename = T.let(nil, T.nilable(Pathname))
return
end
root_url = args.root_url
# GitHub Releases url
root_url ||= if tap.present? && !tap.core_tap? && !args.test_default_formula?
"#{tap.default_remote}/releases/download/#{formula.name}-#{formula.pkg_version}"
root_url ||= if tap.present? && !T.must(tap).core_tap? && !args.test_default_formula?
"#{T.must(tap).default_remote}/releases/download/#{formula.name}-#{formula.pkg_version}"
end
# This is needed where sparse files may be handled (bsdtar >=3.0).
@@ -275,7 +308,7 @@ module Homebrew
verify_local_bottles
test "brew", "bottle", *bottle_args
bottle_step = steps.last
bottle_step = steps.fetch(-1)
if !bottle_step.passed? || !bottle_step.output?
failed formula.full_name, "bottling failed" unless args.dry_run?
@@ -283,8 +316,8 @@ module Homebrew
end
@bottle_filename = Pathname.new(
bottle_step.output
.gsub(%r{.*(\./\S+#{HOMEBREW_BOTTLES_EXTNAME_REGEX}).*}om, '\1'),
T.must(bottle_step.output)
.gsub(%r{.*(\./\S+#{HOMEBREW_BOTTLES_EXTNAME_REGEX}).*}om, '\1'),
)
@bottle_json_filename = Pathname.new(
@bottle_filename.to_s.gsub(/\.(\d+\.)?tar\.gz$/, ".json"),
@@ -296,7 +329,7 @@ module Homebrew
@bottle_output_path.write(bottle_step.output, mode: "a")
bottle_merge_args =
["--merge", "--write", "--no-commit", "--no-all-checks", @bottle_json_filename]
["--merge", "--write", "--no-commit", "--no-all-checks", @bottle_json_filename.to_s]
bottle_merge_args << "--keep-old" if args.keep_old? && !new_formula
test "brew", "bottle", *bottle_merge_args
@@ -314,12 +347,13 @@ module Homebrew
else
ENV.fetch("HOMEBREW_VERIFY_ATTESTATIONS", nil)
end
test "brew", "install", "--only-dependencies", @bottle_filename,
test "brew", "install", "--only-dependencies", @bottle_filename.to_s,
env: { "HOMEBREW_VERIFY_ATTESTATIONS" => verify_attestations }
test "brew", "install", @bottle_filename,
test "brew", "install", @bottle_filename.to_s,
env: { "HOMEBREW_VERIFY_ATTESTATIONS" => verify_attestations }
end
sig { params(formula: Formula, args: Homebrew::Cmd::TestBotCmd::Args).returns(T::Boolean) }
def build_bottle?(formula, args:)
# Build and runtime dependencies must be bottled on the current OS,
# but accept an older compatible bottle for test dependencies.
@@ -334,6 +368,7 @@ module Homebrew
!args.build_from_source?
end
sig { params(formula: Formula).void }
def livecheck(formula)
return unless formula.livecheck_defined?
return if formula.livecheck.skip?
@@ -344,7 +379,7 @@ module Homebrew
return if livecheck_step.failed?
return unless livecheck_step.output?
livecheck_info = JSON.parse(livecheck_step.output)&.first
livecheck_info = JSON.parse(T.must(livecheck_step.output)).first
if livecheck_info["status"] == "error"
error_msg = if livecheck_info["messages"].present? && livecheck_info["messages"].length.positive?
@@ -395,231 +430,236 @@ module Homebrew
end
end
sig { params(formula_name: String, args: Homebrew::Cmd::TestBotCmd::Args).void }
def formula!(formula_name, args:)
cleanup_during!(@testing_formulae, args:)
test_header(:Formulae, method: "formula!(#{formula_name})")
formula = Formulary.factory(formula_name)
if formula.disabled?
skipped formula_name, "#{formula.full_name} has been disabled!"
return
end
begin
if formula.disabled?
skipped formula_name, "#{formula.full_name} has been disabled!"
return
end
test "brew", "deps", "--tree", "--prune", "--annotate", "--include-build", "--include-test",
named_args: formula_name
test "brew", "deps", "--tree", "--prune", "--annotate", "--include-build", "--include-test",
named_args: formula_name
deps_without_compatible_bottles = formula.deps.map(&:to_formula)
deps_without_compatible_bottles.reject! do |dep|
bottled_or_built?(dep, @built_formulae - @skipped_or_failed_formulae)
end
bottled_on_current_version = bottled?(formula, no_older_versions: true)
deps_without_compatible_bottles = formula.deps.map(&:to_formula)
deps_without_compatible_bottles.reject! do |dep|
bottled_or_built?(dep, @built_formulae - @skipped_or_failed_formulae)
end
bottled_on_current_version = bottled?(formula, no_older_versions: true)
if deps_without_compatible_bottles.present? && !bottled_on_current_version
message = <<~EOS
#{formula_name} has dependencies without compatible bottles:
#{deps_without_compatible_bottles * "\n "}
EOS
skipped formula_name, message
return
end
if deps_without_compatible_bottles.present? && !bottled_on_current_version
message = <<~EOS
#{formula_name} has dependencies without compatible bottles:
#{deps_without_compatible_bottles * "\n "}
EOS
skipped formula_name, message
return
end
new_formula = @added_formulae.include?(formula_name)
ignore_failures = !args.test_default_formula? && !bottled_on_current_version && !new_formula
new_formula = @added_formulae.include?(formula_name)
ignore_failures = !args.test_default_formula? && !bottled_on_current_version && !new_formula
deps = []
reqs = []
deps = []
reqs = []
build_flag = if build_bottle?(formula, args:)
"--build-bottle"
else
if GitHub::Actions.env_set?
puts GitHub::Actions::Annotation.new(
:warning,
"#{formula} has unbottled dependencies, so a bottle will not be built.",
title: "No bottle built for #{formula}!",
file: formula.path.to_s.delete_prefix("#{repository}/"),
)
build_flag = if build_bottle?(formula, args:)
"--build-bottle"
else
onoe "Not building a bottle for #{formula} because it has unbottled dependencies."
if GitHub::Actions.env_set?
puts GitHub::Actions::Annotation.new(
:warning,
"#{formula} has unbottled dependencies, so a bottle will not be built.",
title: "No bottle built for #{formula}!",
file: formula.path.to_s.delete_prefix("#{repository}/"),
)
else
onoe "Not building a bottle for #{formula} because it has unbottled dependencies."
end
skipped formula_name, "No bottle built."
return
end
skipped formula_name, "No bottle built."
return
end
# Online checks are a bit flaky and less useful for PRs that modify multiple formulae.
skip_online_checks = args.skip_online_checks? || (@testing_formulae_count > 5)
# Online checks are a bit flaky and less useful for PRs that modify multiple formulae.
skip_online_checks = args.skip_online_checks? || (@testing_formulae_count > 5)
fetch_args = [formula_name]
fetch_args << build_flag
fetch_args << "--force" if cleanup?(args)
fetch_args = [formula_name]
fetch_args << build_flag
fetch_args << "--force" if cleanup?(args)
audit_args = [formula_name]
audit_args << "--online" unless skip_online_checks
if new_formula
if !args.skip_new?
audit_args << "--new"
elsif !args.skip_new_strict?
audit_args << "--strict"
audit_args = [formula_name]
audit_args << "--online" unless skip_online_checks
if new_formula
if !args.skip_new?
audit_args << "--new"
elsif !args.skip_new_strict?
audit_args << "--strict"
end
else
audit_args << "--git" << "--skip-style"
audit_args << "--except=unconfirmed_checksum_change" if args.skip_checksum_only_audit?
audit_args << "--except=stable_version" if args.skip_stable_version_audit?
audit_args << "--except=revision" if args.skip_revision_audit?
end
else
audit_args << "--git" << "--skip-style"
audit_args << "--except=unconfirmed_checksum_change" if args.skip_checksum_only_audit?
audit_args << "--except=stable_version" if args.skip_stable_version_audit?
audit_args << "--except=revision" if args.skip_revision_audit?
end
# This needs to be done before any network operation.
install_ca_certificates_if_needed
# This needs to be done before any network operation.
install_ca_certificates_if_needed
if (messages = unsatisfied_requirements_messages(formula))
test "brew", "fetch", "--formula", "--retry", *fetch_args
test "brew", "audit", "--formula", *audit_args
skipped formula_name, messages
return
end
deps |= formula.deps.to_a.reject(&:optional?)
reqs |= formula.requirements.to_a.reject(&:optional?)
tap_needed_taps(deps)
install_curl_if_needed(formula)
install_mercurial_if_needed(deps, reqs)
install_subversion_if_needed(deps, reqs)
setup_formulae_deps_instances(formula, formula_name, args:)
test "brew", "uninstall", "--formula", "--force", formula_name if formula.latest_version_installed?
install_args = ["--verbose", "--formula"]
install_args << build_flag
# We can't verify attestations if we're building `gh`.
verify_attestations = if formula_name == "gh"
nil
else
ENV.fetch("HOMEBREW_VERIFY_ATTESTATIONS", nil)
end
# Don't care about e.g. bottle failures for dependencies.
test "brew", "install", "--only-dependencies", *install_args, formula_name,
env: { "HOMEBREW_DEVELOPER" => nil,
"HOMEBREW_VERIFY_ATTESTATIONS" => verify_attestations }
info_header "Starting tests for #{formula_name}"
if (messages = unsatisfied_requirements_messages(formula))
test "brew", "fetch", "--formula", "--retry", *fetch_args
test "brew", "audit", "--formula", *audit_args
skipped formula_name, messages
return
end
deps |= formula.deps.to_a.reject(&:optional?)
reqs |= formula.requirements.to_a.reject(&:optional?)
tap_needed_taps(deps)
install_curl_if_needed(formula)
install_mercurial_if_needed(deps, reqs)
install_subversion_if_needed(deps, reqs)
setup_formulae_deps_instances(formula, formula_name, args:)
test "brew", "uninstall", "--formula", "--force", formula_name if formula.latest_version_installed?
install_args = ["--verbose", "--formula"]
install_args << build_flag
# We can't verify attestations if we're building `gh`.
verify_attestations = if formula_name == "gh"
nil
else
ENV.fetch("HOMEBREW_VERIFY_ATTESTATIONS", nil)
end
# Don't care about e.g. bottle failures for dependencies.
test "brew", "install", "--only-dependencies", *install_args, formula_name,
env: { "HOMEBREW_DEVELOPER" => nil,
"HOMEBREW_VERIFY_ATTESTATIONS" => verify_attestations }
info_header "Starting tests for #{formula_name}"
test "brew", "fetch", "--formula", "--retry", *fetch_args
env = {}
env["HOMEBREW_GIT_PATH"] = nil if deps.any? do |d|
d.name == "git" && (!d.test? || d.build?)
end
install_step_passed = formula_installed_from_bottle =
artifact_cache_valid?(formula) &&
verify_local_bottles && # Checking the artifact cache loads formulae, so do this check second.
install_formula_from_bottle!(formula_name,
bottle_dir: artifact_cache,
testing_formulae_dependents: false,
dry_run: args.dry_run?)
install_step_passed ||= begin
test("brew", "install", *install_args,
named_args: formula_name,
env: env.merge({ "HOMEBREW_DEVELOPER" => nil,
"HOMEBREW_VERIFY_ATTESTATIONS" => verify_attestations }),
ignore_failures:, report_analytics: true)
steps.last.passed?
end
livecheck(formula) if !args.skip_livecheck? && !skip_online_checks
test "brew", "style", "--formula", formula_name, report_analytics: true
test "brew", "audit", "--formula", *audit_args, report_analytics: true unless formula.deprecated?
unless install_step_passed
if ignore_failures
skipped formula_name, "install failed"
else
failed formula_name, "install failed"
end
return
end
if formula_installed_from_bottle
moved_artifacts = bottle_glob(formula_name, artifact_cache, ".{json,tar.gz}").map(&:realpath)
Pathname.pwd.install moved_artifacts
moved_artifacts.each do |old_location|
new_location = old_location.basename.realpath
@bottle_checksums[new_location] = @bottle_checksums.fetch(old_location)
@bottle_checksums.delete(old_location)
end
else
bottle_reinstall_formula(formula, new_formula, args:)
end
@built_formulae << formula.full_name
test("brew", "linkage", "--test", named_args: formula_name, ignore_failures:, report_analytics: true)
failed_linkage_or_test_messages ||= []
failed_linkage_or_test_messages << "linkage failed" unless steps.last.passed?
if steps.last.passed?
# Check for opportunistic linkage. Ignore failures because
# they can be unavoidable but we still want to know about them.
test "brew", "linkage", "--cached", "--test", "--strict",
named_args: formula_name,
ignore_failures: !args.test_default_formula?
end
test "brew", "linkage", "--cached", formula_name
@linkage_output_path.write(Formatter.headline(steps.last.command_trimmed, color: :blue), mode: "a")
@linkage_output_path.write("\n", mode: "a")
@linkage_output_path.write(steps.last.output, mode: "a")
test "brew", "install", "--formula", "--only-dependencies", "--include-test", formula_name
if formula.test_defined?
env = {}
env["HOMEBREW_GIT_PATH"] = nil if deps.any? do |d|
d.name == "git" && (!d.build? || d.test?)
d.name == "git" && (!d.test? || d.build?)
end
# Intentionally not passing --retry here to avoid papering over
# flaky tests when a formula isn't being pulled in as a dependent.
test("brew", "test", "--verbose", named_args: formula_name, env:, ignore_failures:,
report_analytics: true)
failed_linkage_or_test_messages << "test failed" unless steps.last.passed?
end
install_step_passed = formula_installed_from_bottle =
artifact_cache_valid?(formula) &&
verify_local_bottles && # Checking the artifact cache loads formulae, so do this check second.
install_formula_from_bottle!(formula_name,
bottle_dir: artifact_cache,
testing_formulae_dependents: false,
dry_run: args.dry_run?)
# Move bottle and don't test dependents if the formula linkage or test failed.
if failed_linkage_or_test_messages.present?
if @bottle_filename
failed_dir = @bottle_filename.dirname/"failed"
moved_artifacts = [@bottle_filename, @bottle_json_filename].map(&:realpath)
failed_dir.install moved_artifacts
install_step_passed ||= begin
test("brew", "install", *install_args,
named_args: formula_name,
env: env.merge({ "HOMEBREW_DEVELOPER" => nil,
"HOMEBREW_VERIFY_ATTESTATIONS" => verify_attestations }),
ignore_failures:, report_analytics: true)
steps.fetch(-1).passed?
end
livecheck(formula) if !args.skip_livecheck? && !skip_online_checks
test "brew", "style", "--formula", formula_name, report_analytics: true
test "brew", "audit", "--formula", *audit_args, report_analytics: true unless formula.deprecated?
unless install_step_passed
if ignore_failures
skipped formula_name, "install failed"
else
failed formula_name, "install failed"
end
return
end
if formula_installed_from_bottle
moved_artifacts = bottle_glob(formula_name, artifact_cache, ".{json,tar.gz}").map(&:realpath)
Pathname.pwd.install moved_artifacts
moved_artifacts.each do |old_location|
new_location = (failed_dir/old_location.basename).realpath
new_location = old_location.basename.realpath
@bottle_checksums[new_location] = @bottle_checksums.fetch(old_location)
@bottle_checksums.delete(old_location)
end
end
if ignore_failures
skipped formula_name, failed_linkage_or_test_messages.join(", ")
else
failed formula_name, failed_linkage_or_test_messages.join(", ")
bottle_reinstall_formula(formula, new_formula, args:)
end
end
ensure
@tested_formulae_count += 1
cleanup_bottle_etc_var(formula) if cleanup?(args)
@built_formulae << formula.full_name
test("brew", "linkage", "--test", named_args: formula_name, ignore_failures:, report_analytics: true)
failed_linkage_or_test_messages ||= []
failed_linkage_or_test_messages << "linkage failed" unless steps.fetch(-1).passed?
if @unchanged_dependencies.present?
test "brew", "uninstall", "--formulae", "--force", "--ignore-dependencies", *@unchanged_dependencies
if steps.fetch(-1).passed?
# Check for opportunistic linkage. Ignore failures because
# they can be unavoidable but we still want to know about them.
test "brew", "linkage", "--cached", "--test", "--strict",
named_args: formula_name,
ignore_failures: !args.test_default_formula?
end
test "brew", "linkage", "--cached", formula_name
@linkage_output_path.write(Formatter.headline(steps.fetch(-1).command_trimmed, color: :blue), mode: "a")
@linkage_output_path.write("\n", mode: "a")
@linkage_output_path.write(steps.fetch(-1).output, mode: "a")
test "brew", "install", "--formula", "--only-dependencies", "--include-test", formula_name
if formula.test_defined?
env = {}
env["HOMEBREW_GIT_PATH"] = nil if deps.any? do |d|
d.name == "git" && (!d.build? || d.test?)
end
# Intentionally not passing --retry here to avoid papering over
# flaky tests when a formula isn't being pulled in as a dependent.
test(
"brew", "test", "--verbose", named_args: formula_name, env:, ignore_failures:, report_analytics: true
)
failed_linkage_or_test_messages << "test failed" unless steps.fetch(-1).passed?
end
# Move bottle and don't test dependents if the formula linkage or test failed.
if failed_linkage_or_test_messages.present?
if @bottle_filename
failed_dir = @bottle_filename.dirname/"failed"
moved_artifacts = [@bottle_filename, T.must(@bottle_json_filename)].map(&:realpath)
failed_dir.install moved_artifacts
moved_artifacts.each do |old_location|
new_location = (failed_dir/old_location.basename).realpath
@bottle_checksums[new_location] = @bottle_checksums.fetch(old_location)
@bottle_checksums.delete(old_location)
end
end
if ignore_failures
skipped formula_name, failed_linkage_or_test_messages.join(", ")
else
failed formula_name, failed_linkage_or_test_messages.join(", ")
end
end
ensure
@tested_formulae_count += 1
cleanup_bottle_etc_var(formula) if cleanup?(args)
if @unchanged_dependencies.present?
test "brew", "uninstall", "--formulae", "--force", "--ignore-dependencies", *@unchanged_dependencies
end
end
end
sig { params(formula_name: String).void }
def portable_formula!(formula_name)
test_header(:Formulae, method: "portable_formula!(#{formula_name})")
@@ -655,6 +695,7 @@ report_analytics: true)
test "brew", "bottle", "--skip-relocation", "--json", "--no-rebuild", formula_name
end
sig { params(formula_name: String).void }
def deleted_formula!(formula_name)
test_header(:Formulae, method: "deleted_formula!(#{formula_name})")
@@ -667,8 +708,9 @@ report_analytics: true)
formula_name
end
sig { returns(T::Boolean) }
def testing_portable_ruby?
tap&.core_tap? && @testing_formulae.include?("portable-ruby")
!!tap&.core_tap? && @testing_formulae.include?("portable-ruby")
end
end
end
@@ -1,11 +1,32 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
module Homebrew
module TestBot
class FormulaeDependents < TestFormulae
attr_writer :testing_formulae, :tested_formulae
sig { params(testing_formulae: T::Array[String]).returns(T::Array[String]) }
attr_writer :testing_formulae
sig { params(tested_formulae: T::Array[String]).returns(T::Array[String]) }
attr_writer :tested_formulae
sig {
params(
tap: T.nilable(Tap),
git: T.nilable(String),
dry_run: T::Boolean,
fail_fast: T::Boolean,
verbose: T::Boolean,
).void
}
def initialize(tap:, git:, dry_run:, fail_fast:, verbose:)
super
@testing_formulae_with_tested_dependents = T.let([], T::Array[String])
@tested_dependents_list = T.let(nil, T.nilable(Pathname))
@dependent_testing_formulae = T.let([], T::Array[String])
end
sig { params(args: Homebrew::Cmd::TestBotCmd::Args).void }
def run!(args:)
test "brew", "untap", "--force", "homebrew/cask" if !tap&.core_cask_tap? && CoreCaskTap.instance.installed?
@@ -33,11 +54,14 @@ module Homebrew
# rubocop:enable Homebrew/MoveToExtendOS
download_artifacts_from_previous_run!("dependents{,_#{artifact_specifier}*}", dry_run: args.dry_run?)
@skip_candidates = if (tested_dependents_cache = artifact_cache/@tested_dependents_list).exist?
tested_dependents_cache.read.split("\n")
else
[]
end
@skip_candidates = T.let(
if (tested_dependents_cache = artifact_cache/@tested_dependents_list).exist?
tested_dependents_cache.read.split("\n")
else
[]
end,
T.nilable(T::Array[String]),
)
@dependent_testing_formulae.each do |formula_name|
dependent_formulae!(formula_name, args:)
@@ -56,6 +80,7 @@ module Homebrew
private
sig { params(installable_bottles: T::Array[String], args: Homebrew::Cmd::TestBotCmd::Args).void }
def install_formulae_if_needed_from_bottles!(installable_bottles, args:)
installable_bottles.each do |formula_name|
formula = Formulary.factory(formula_name)
@@ -65,6 +90,7 @@ module Homebrew
end
end
sig { params(formula_name: String, args: Homebrew::Cmd::TestBotCmd::Args).void }
def dependent_formulae!(formula_name, args:)
cleanup_during!(@dependent_testing_formulae, args:)
@@ -92,13 +118,13 @@ module Homebrew
named_args: formula_name,
ignore_failures: !bottled?(formula, no_older_versions: true),
env: { "HOMEBREW_DEVELOPER" => nil }
return unless steps.last.passed?
return unless steps.fetch(-1).passed?
# Restore etc/var files that may have been nuked in the build stage.
test "brew", "postinstall",
named_args: formula_name,
ignore_failures: !bottled?(formula, no_older_versions: true)
return unless steps.last.passed?
return unless steps.fetch(-1).passed?
# Test texlive first to avoid GitHub-hosted runners running out of storage.
# TODO: Try generalising this by sorting dependents according to install size,
@@ -121,6 +147,10 @@ module Homebrew
end
end
sig {
params(formula: Formula, formula_name: String, args: Homebrew::Cmd::TestBotCmd::Args)
.returns([T::Array[Formula], T::Array[Formula], T::Array[Formula]])
}
def dependents_for_formula(formula, formula_name, args:)
info_header "Determining dependents..."
@@ -224,17 +254,25 @@ module Homebrew
[source_dependents, bottled_dependents, testable_dependents]
end
sig {
params(
dependent: Formula,
testable_dependents: T::Array[Formula],
args: Homebrew::Cmd::TestBotCmd::Args,
build_from_source: T::Boolean,
).void
}
def install_dependent(dependent, testable_dependents, args:, build_from_source: false)
if @skip_candidates.include?(dependent.full_name) &&
if @skip_candidates&.include?(dependent.full_name) &&
artifact_cache_valid?(dependent, formulae_dependents: true)
@tested_dependents_list.write(dependent.full_name, mode: "a")
@tested_dependents_list.write("\n", mode: "a")
@tested_dependents_list&.write(dependent.full_name, mode: "a")
@tested_dependents_list&.write("\n", mode: "a")
skipped dependent.name, "#{dependent.full_name} has been tested at #{previous_github_sha}"
return
end
if (messages = unsatisfied_requirements_messages(dependent))
skipped dependent, messages
skipped dependent.name, messages
return
end
@@ -277,14 +315,14 @@ module Homebrew
build_args << "--build-from-source"
test "brew", "fetch", "--build-from-source", "--retry", dependent.full_name
return if steps.last.failed?
return if steps.fetch(-1).failed?
else
fetch_formulae << dependent.full_name
end
if fetch_formulae.present?
test "brew", "fetch", "--retry", *fetch_formulae
return if steps.last.failed?
return if steps.fetch(-1).failed?
end
unlink_conflicts dependent
@@ -302,7 +340,7 @@ module Homebrew
named_args: dependent.full_name,
env: env.merge({ "HOMEBREW_DEVELOPER" => nil }),
ignore_failures: !args.test_default_formula? && !bottled_on_current_version
install_step = steps.last
install_step = steps.fetch(-1)
return unless install_step.passed?
end
@@ -316,7 +354,7 @@ module Homebrew
test "brew", "linkage", "--test",
named_args: dependent.full_name,
ignore_failures: !args.test_default_formula? && !bottled_on_current_version
linkage_step = steps.last
linkage_step = steps.fetch(-1)
if linkage_step.passed? && !build_from_source
# Check for opportunistic linkage. Ignore failures because
@@ -346,18 +384,18 @@ module Homebrew
named_args: dependent.full_name,
env:,
ignore_failures: !args.test_default_formula? && !bottled_on_current_version
test_step = steps.last
test_step = steps.fetch(-1)
end
test "brew", "uninstall", "--force", "--ignore-dependencies", dependent.full_name
all_tests_passed = (dependent_was_previously_installed || install_step.passed?) &&
linkage_step.passed? &&
(testable_dependents.exclude?(dependent) || test_step.passed?)
(testable_dependents.exclude?(dependent) || test_step&.passed?)
if all_tests_passed
@tested_dependents_list.write(dependent.full_name, mode: "a")
@tested_dependents_list.write("\n", mode: "a")
@tested_dependents_list&.write(dependent.full_name, mode: "a")
@tested_dependents_list&.write("\n", mode: "a")
end
return unless GitHub::Actions.env_set?
@@ -388,6 +426,7 @@ module Homebrew
end
end
sig { params(formula: Formula).void }
def unlink_conflicts(formula)
return if formula.keg_only?
return if formula.linked_keg.exist?
+38 -13
View File
@@ -1,20 +1,33 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
module Homebrew
module TestBot
class FormulaeDetect < Test
sig { returns(T::Array[String]) }
attr_reader :testing_formulae, :added_formulae, :deleted_formulae
sig {
params(
argument: String,
tap: T.nilable(Tap),
git: String,
dry_run: T::Boolean,
fail_fast: T::Boolean,
verbose: T::Boolean,
).void
}
def initialize(argument, tap:, git:, dry_run:, fail_fast:, verbose:)
super(tap:, git:, dry_run:, fail_fast:, verbose:)
@argument = argument
@added_formulae = []
@deleted_formulae = []
@formulae_to_fetch = []
@added_formulae = T.let([], T::Array[String])
@deleted_formulae = T.let([], T::Array[String])
@formulae_to_fetch = T.let([], T::Array[String])
@testing_formulae = T.let([], T::Array[String])
end
sig { params(args: Homebrew::Cmd::TestBotCmd::Args).void }
def run!(args:)
detect_formulae!(args:)
@@ -30,6 +43,7 @@ module Homebrew
private
sig { params(args: Homebrew::Cmd::TestBotCmd::Args).void }
def detect_formulae!(args:)
test_header(:FormulaeDetect, method: :detect_formulae!)
@@ -69,11 +83,11 @@ module Homebrew
No known CI provider detected! If you are using GitHub Actions then we cannot find the expected environment variables! Check you have e.g. exported them to a Docker container.
EOS
end
elsif tap.present? && tap.full_name.casecmp(github_repository).zero?
elsif (tap = self.tap.presence) && tap.full_name.casecmp(github_repository)&.zero?
# Use GitHub Actions variables for pull request jobs.
if (base_ref = ENV.fetch("GITHUB_BASE_REF", nil)).present?
unless tap.official?
test git, "-C", repository, "fetch",
test git.to_s, "-C", repository.to_s, "fetch",
"origin", "+refs/heads/#{base_ref}"
end
origin_ref = "origin/#{base_ref}"
@@ -86,7 +100,7 @@ module Homebrew
diff_end_sha1 = github_sha
# Use GitHub Actions variables for branch jobs.
else
test git, "-C", repository, "fetch", "origin", "+#{github_ref}" unless tap.official?
test git.to_s, "-C", repository.to_s, "fetch", "origin", "+#{github_ref}" unless tap.official?
origin_ref = "origin/#{github_ref.gsub(%r{^refs/heads/}, "")}"
diff_end_sha1 = diff_start_sha1 = github_sha
end
@@ -104,7 +118,7 @@ module Homebrew
diff_start_sha1 = diff_end_sha1 if @testing_formulae.present?
if tap
if (tap = self.tap.presence)
tap_origin_ref_revision_args =
[git, "-C", tap.path.to_s, "log", "-1", "--format=%h (%s)", origin_ref]
tap_origin_ref_revision = if args.dry_run?
@@ -129,7 +143,7 @@ module Homebrew
modified_formulae = []
if tap && diff_start_sha1 != diff_end_sha1
if diff_start_sha1 != diff_end_sha1 && (tap = self.tap.presence)
formula_path = tap.formula_dir.to_s
@added_formulae +=
diff_formulae(diff_start_sha1, diff_end_sha1, formula_path, "A")
@@ -205,6 +219,7 @@ module Homebrew
EOS
end
sig { params(formula_name: String, args: Homebrew::Cmd::TestBotCmd::Args).returns(T.nilable(String)) }
def safe_formula_canonical_name(formula_name, args:)
Homebrew.with_no_api_env do
Formulary.factory(formula_name).full_name
@@ -213,7 +228,7 @@ module Homebrew
raise if e.tap.installed?
test "brew", "tap", e.tap.name
retry unless steps.last.failed?
retry unless steps.fetch(-1).failed?
onoe e
puts e.backtrace if args.debug?
rescue FormulaUnavailableError, TapFormulaAmbiguityError => e
@@ -221,26 +236,36 @@ module Homebrew
puts e.backtrace if args.debug?
end
sig { params(ref: String).returns(String) }
def rev_parse(ref)
Utils.popen_read(git, "-C", repository, "rev-parse", "--verify", ref).strip
end
sig { returns(String) }
def current_sha1
rev_parse("HEAD")
end
sig {
params(
start_revision: String,
end_revision: String,
path: String,
filter: String,
).returns(T::Array[String])
}
def diff_formulae(start_revision, end_revision, path, filter)
return unless tap
raise "A tap is required to call diff_formulae" unless @tap
Utils.safe_popen_read(
git, "-C", repository,
"diff-tree", "-r", "--name-only", "--diff-filter=#{filter}",
start_revision, end_revision, "--", path
).lines(chomp: true).filter_map do |file|
next unless tap.formula_file?(file)
next unless @tap.formula_file?(file)
file = Pathname.new(file)
tap.formula_file_to_name(file)
@tap.formula_file_to_name(file)
end
end
end
+8 -4
View File
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
module Homebrew
@@ -6,10 +6,13 @@ module Homebrew
# Creates Junit report with only required by BuildPulse attributes
# See https://github.com/Homebrew/homebrew-test-bot/pull/621#discussion_r658712640
class Junit
sig { params(tests: T::Array[Test]).void }
def initialize(tests)
@tests = tests
@xml_document = T.let(nil, T.nilable(REXML::Document))
end
sig { params(filters: T.nilable(T::Array[String])).void }
def build(filters: nil)
filters ||= []
@@ -26,7 +29,7 @@ module Homebrew
testsuite = testsuites.add_element "testsuite"
testsuite.add_attribute "name", "brew-test-bot.#{Utils::Bottles.tag}"
testsuite.add_attribute "timestamp", test.steps.first.start_time.iso8601
testsuite.add_attribute "timestamp", T.must(test.steps.fetch(0).start_time).iso8601
test.steps.each do |step|
next unless filters.any? { |filter| step.command_short.start_with? filter }
@@ -35,7 +38,7 @@ module Homebrew
testcase.add_attribute "name", step.command_short
testcase.add_attribute "status", step.status
testcase.add_attribute "time", step.time
testcase.add_attribute "timestamp", step.start_time.iso8601
testcase.add_attribute "timestamp", T.must(step.start_time).iso8601
next if step.passed?
@@ -45,12 +48,13 @@ module Homebrew
end
end
sig { params(filename: String).void }
def write(filename)
output_path = Pathname(filename)
output_path.unlink if output_path.exist?
output_path.open("w") do |xml_file|
pretty_print_indent = 2
@xml_document.write(xml_file, pretty_print_indent)
T.must(@xml_document).write(xml_file, pretty_print_indent)
end
end
end
+2 -1
View File
@@ -1,9 +1,10 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
module Homebrew
module TestBot
class Setup < Test
sig { params(args: Homebrew::Cmd::TestBotCmd::Args).returns(Step) }
def run!(args:)
test_header(:Setup)
+59 -21
View File
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "system_command"
@@ -11,24 +11,48 @@ module Homebrew
class Step
include SystemCommand::Mixin
attr_reader :command, :name, :status, :output, :start_time, :end_time
sig { returns(T::Array[String]) }
attr_reader :command
sig { returns(T.nilable(String)) }
attr_reader :name
sig { returns(Symbol) }
attr_reader :status
sig { returns(T.nilable(String)) }
attr_reader :output
sig { returns(T.nilable(Time)) }
attr_reader :start_time, :end_time
# Instantiates a Step object.
# @param command [Array<String>] Command to execute and arguments.
# @param env [Hash] Environment variables to set when running command.
# @param command Command to execute and arguments.
# @param env Environment variables to set when running command.
sig {
params(
command: T::Array[String],
env: T::Hash[String, String],
verbose: T::Boolean,
named_args: T.nilable(T.any(String, T::Array[String])),
ignore_failures: T::Boolean,
repository: T.nilable(Pathname),
).void
}
def initialize(command, env:, verbose:, named_args: nil, ignore_failures: false, repository: nil)
@named_args = [named_args].flatten.compact.map(&:to_s)
@command = command + @named_args
@named_args = T.let([named_args].flatten.compact.map(&:to_s), T::Array[String])
@command = T.let(command + @named_args, T::Array[String])
@env = env
@verbose = verbose
@ignore_failures = ignore_failures
@repository = repository
@name = command[1]&.delete("-")
@status = :running
@output = nil
@name = T.let(command[1]&.delete("-"), T.nilable(String))
@status = T.let(:running, Symbol)
@output = T.let(nil, T.nilable(String))
end
sig { returns(String) }
def command_trimmed
command.reject { |arg| arg.to_s.start_with?("--exclude") }
.join(" ")
@@ -37,6 +61,7 @@ module Homebrew
.delete_prefix("/usr/bin/")
end
sig { returns(String) }
def command_short
(@command - %W[
brew
@@ -56,26 +81,32 @@ module Homebrew
.gsub(Dir.pwd, "")
end
sig { returns(T::Boolean) }
def passed?
@status == :passed
end
sig { returns(T::Boolean) }
def failed?
@status == :failed
end
sig { returns(T::Boolean) }
def ignored?
@status == :ignored
end
sig { void }
def puts_command
puts Formatter.headline(command_trimmed, color: :blue)
end
sig { void }
def puts_result
puts Formatter.headline(Formatter.error("FAILED"), color: :red) unless passed?
end
sig { params(message: String, title: String, file: String, line: T.nilable(Integer)).void }
def puts_github_actions_annotation(message, title, file, line)
return unless GitHub::Actions.env_set?
@@ -91,23 +122,27 @@ module Homebrew
puts annotation
end
def puts_in_github_actions_group(title)
sig { params(title: String, _block: T.proc.void).void }
def puts_in_github_actions_group(title, &_block)
puts "::group::#{title}" if GitHub::Actions.env_set?
yield
puts "::endgroup::" if GitHub::Actions.env_set?
end
sig { returns(T::Boolean) }
def output?
@output.present?
end
# The execution time of the task.
# Precondition: Step#run has been called.
# @return [Float] execution time in seconds
# @return execution time in seconds
sig { returns(Float) }
def time
end_time - start_time
T.must(end_time) - T.must(start_time)
end
sig { void }
def puts_full_output
return if @output.blank? || @verbose
@@ -116,9 +151,10 @@ module Homebrew
end
end
sig { params(name: String).returns([Pathname, T.nilable(Integer)]) }
def annotation_location(name)
formula = Formulary.factory(name)
method_sym = command.second.to_sym
method_sym = command.fetch(1).to_sym
method_location = formula.method(method_sym).source_location if formula.respond_to?(method_sym)
if method_location.present? && (method_location.first == formula.path.to_s)
@@ -127,9 +163,10 @@ module Homebrew
[formula.path, nil]
end
rescue FormulaUnavailableError
[@repository.glob("**/#{name}*").first, nil]
[@repository&.glob("**/#{name}*")&.first, nil]
end
sig { params(output: String, max_kb: Integer, context_lines: Integer).returns(String) }
def truncate_output(output, max_kb:, context_lines:)
output_lines = output.lines
first_error_index = output_lines.find_index do |line|
@@ -152,12 +189,13 @@ module Homebrew
else
start = [first_error_index - context_lines, 0].max
# Let GitHub Actions truncate us to 4KB if needed.
output_lines[start..].join
T.must(output_lines[start..]).join
end
end
sig { params(dry_run: T::Boolean, fail_fast: T::Boolean).void }
def run(dry_run: false, fail_fast: false)
@start_time = Time.now
@start_time = T.let(Time.now, T.nilable(Time))
puts_command
if dry_run
@@ -170,12 +208,12 @@ module Homebrew
executable, *args = command
result = system_command executable, args:,
print_stdout: @verbose,
print_stderr: @verbose,
env: @env
result = system_command T.must(executable), args:,
print_stdout: @verbose,
print_stderr: @verbose,
env: @env
@end_time = Time.now
@end_time = T.let(Time.now, T.nilable(Time))
@status = if result.success?
:passed
+11 -9
View File
@@ -1,30 +1,32 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
module Homebrew
module TestBot
class TapSyntax < Test
sig { params(args: Homebrew::Cmd::TestBotCmd::Args).void }
def run!(args:)
test_header(:TapSyntax)
return unless tap.installed?
tapped = T.must(tap)
return unless tapped.installed?
unless args.stable?
# Run `brew typecheck` if this tap is typed.
# TODO: consider in future if we want to allow unsupported taps here.
if tap.official? && quiet_system(git, "-C", tap.path.to_s, "grep", "-qE",
"^# typed: (true|strict|strong)$")
test "brew", "typecheck", tap.name
if tapped.official? && quiet_system(git, "-C", tapped.path.to_s, "grep", "-qE",
"^# typed: (true|strict|strong)$")
test "brew", "typecheck", tapped.name
end
test "brew", "style", tap.name
test "brew", "style", tapped.name
end
return if tap.formula_files.blank? && tap.cask_files.blank?
return if tapped.formula_files.blank? && tapped.cask_files.blank?
test "brew", "readall", "--aliases", "--os=all", "--arch=all", tap.name
test "brew", "readall", "--aliases", "--os=all", "--arch=all", tapped.name
return if args.stable?
test "brew", "audit", "--except=installed", "--tap=#{tap.name}"
test "brew", "audit", "--except=installed", "--tap=#{tapped.name}"
end
end
end
+38 -8
View File
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "utils/analytics"
@@ -9,30 +9,51 @@ module Homebrew
class Test
include Utils::Output::Mixin
sig { returns(T::Array[Step]) }
def failed_steps
@steps.select(&:failed?)
end
sig { returns(T::Array[Step]) }
def ignored_steps
@steps.select(&:ignored?)
end
sig { returns(T::Array[Step]) }
attr_reader :steps
protected
sig { params(args: Homebrew::Cmd::TestBotCmd::Args).returns(T::Boolean) }
def cleanup?(args)
Homebrew::TestBot.cleanup?(args)
end
sig { params(args: Homebrew::Cmd::TestBotCmd::Args).returns(T::Boolean) }
def local?(args)
Homebrew::TestBot.local?(args)
end
private
attr_reader :tap, :git, :repository
sig { returns(T.nilable(Tap)) }
attr_reader :tap
sig { returns(T.nilable(String)) }
attr_reader :git
sig { returns(Pathname) }
attr_reader :repository
sig {
params(
tap: T.nilable(Tap),
git: T.nilable(String),
dry_run: T::Boolean,
fail_fast: T::Boolean,
verbose: T::Boolean,
).void
}
def initialize(tap: nil, git: nil, dry_run: false, fail_fast: false, verbose: false)
@tap = tap
@git = git
@@ -40,24 +61,33 @@ module Homebrew
@fail_fast = fail_fast
@verbose = verbose
@steps = []
@steps = T.let([], T::Array[Step])
@repository = if @tap
@tap.path
else
CoreTap.instance.path
end
tap_path = @tap ? @tap.path : CoreTap.instance.path
@repository = T.let(tap_path, Pathname)
end
sig { params(klass: Symbol, method: T.nilable(T.any(String, Symbol))).void }
def test_header(klass, method: "run!")
puts
puts Formatter.headline("Running #{klass}##{method}", color: :magenta)
end
sig { params(text: String).void }
def info_header(text)
puts Formatter.headline(text, color: :cyan)
end
sig {
params(
arguments: String,
named_args: T.nilable(T.any(String, T::Array[String])),
env: T::Hash[String, String],
verbose: T::Boolean,
ignore_failures: T::Boolean,
report_analytics: T::Boolean,
).returns(Step)
}
def test(*arguments, named_args: nil, env: {}, verbose: @verbose, ignore_failures: false,
report_analytics: false)
step = Step.new(
+27 -17
View File
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "os"
@@ -9,21 +9,23 @@ module Homebrew
class TestCleanup < Test
protected
ALLOWED_TAPS = [
ALLOWED_TAPS = T.let([
CoreTap.instance.name,
CoreCaskTap.instance.name,
].freeze
].freeze, T::Array[String])
sig { params(repository: String).void }
def reset_if_needed(repository)
default_ref = default_origin_ref(repository)
return if system(git, "-C", repository, "diff", "--quiet", default_ref)
return if system(git.to_s, "-C", repository, "diff", "--quiet", default_ref)
test git, "-C", repository, "reset", "--hard", default_ref
test git.to_s, "-C", repository, "reset", "--hard", default_ref
end
# Moving files is faster than removing them,
# so move them if the current runner is ephemeral.
sig { params(paths: T::Array[Pathname], sudo: T::Boolean).void }
def delete_or_move(paths, sudo: false)
return if paths.blank?
@@ -44,7 +46,7 @@ module Homebrew
else
paths.each do |path|
if sudo
test "sudo", "mv", path, Dir.mktmpdir
test "sudo", "mv", path.to_s, Dir.mktmpdir
else
FileUtils.mv path, Dir.mktmpdir, force: true
end
@@ -52,13 +54,15 @@ module Homebrew
end
end
sig { void }
def cleanup_shared
FileUtils.chmod_R("u+X", HOMEBREW_CELLAR, force: true)
if repository.exist?
cleanup_git_meta(repository)
clean_if_needed(repository)
prune_if_needed(repository)
repo = repository.to_s
cleanup_git_meta(repo)
clean_if_needed(repo)
prune_if_needed(repo)
end
if HOMEBREW_REPOSITORY != HOMEBREW_PREFIX
@@ -113,13 +117,14 @@ module Homebrew
delete_or_move taps_to_remove
Pathname.glob("#{HOMEBREW_LIBRARY}/Taps/*/*").each do |git_repo|
cleanup_git_meta(git_repo)
git_repo_str = git_repo.to_s
cleanup_git_meta(git_repo_str)
next if repository == git_repo
checkout_branch_if_needed(git_repo)
reset_if_needed(git_repo)
clean_if_needed(git_repo)
prune_if_needed(git_repo)
checkout_branch_if_needed(git_repo_str)
reset_if_needed(git_repo_str)
clean_if_needed(git_repo_str)
prune_if_needed(git_repo_str)
end
# don't need to do `brew cleanup` unless we're self-hosted.
@@ -130,6 +135,7 @@ module Homebrew
private
sig { params(repository: String).returns(String) }
def default_origin_ref(repository)
default_branch = Utils.popen_read(
git, "-C", repository, "symbolic-ref", "refs/remotes/origin/HEAD", "--short"
@@ -138,6 +144,7 @@ module Homebrew
default_branch
end
sig { params(repository: String).void }
def checkout_branch_if_needed(repository)
# We limit this to two parts, because branch names can have slashes in
default_branch = default_origin_ref(repository).split("/", 2).last
@@ -146,15 +153,17 @@ module Homebrew
).strip
return if default_branch == current_branch
test git, "-C", repository, "checkout", "-f", default_branch
test git.to_s, "-C", repository, "checkout", "-f", default_branch.to_s
end
sig { params(repository: String).void }
def cleanup_git_meta(repository)
pr_locks = "#{repository}/.git/refs/remotes/*/pr/*/*.lock"
Dir.glob(pr_locks) { |lock| FileUtils.rm_f lock }
FileUtils.rm_f "#{repository}/.git/gc.log"
end
sig { params(repository: String).void }
def clean_if_needed(repository)
return if repository == HOMEBREW_PREFIX && HOMEBREW_PREFIX != HOMEBREW_REPOSITORY
@@ -168,15 +177,16 @@ module Homebrew
git, "-C", repository, "clean", "--dry-run", *clean_args
).strip.empty?
test git, "-C", repository, "clean", "-ff", *clean_args
test git.to_s, "-C", repository, "clean", "-ff", *clean_args
end
sig { params(repository: String).void }
def prune_if_needed(repository)
return unless Utils.safe_popen_read(
"#{git} -C '#{repository}' -c gc.autoDetach=false gc --auto 2>&1",
).include?("git prune")
test git, "-C", repository, "prune"
test git.to_s, "-C", repository, "prune"
end
end
end
+91 -21
View File
@@ -1,36 +1,53 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
module Homebrew
module TestBot
class TestFormulae < Test
sig { returns(T::Array[String]) }
attr_accessor :skipped_or_failed_formulae
sig { returns(Pathname) }
attr_reader :artifact_cache
sig {
params(
tap: T.nilable(Tap),
git: T.nilable(String),
dry_run: T::Boolean,
fail_fast: T::Boolean,
verbose: T::Boolean,
).void
}
def initialize(tap:, git:, dry_run:, fail_fast:, verbose:)
super
@skipped_or_failed_formulae = []
@artifact_cache = Pathname.new("artifact-cache")
@skipped_or_failed_formulae = T.let([], T::Array[String])
@artifact_cache = T.let(Pathname.new("artifact-cache"), Pathname)
# Let's keep track of the artifacts we've already downloaded
# to avoid repeatedly trying to download the same thing.
@downloaded_artifacts = Hash.new { |h, k| h[k] = [] }
@downloaded_artifacts = T.let(Hash.new { |h, k| h[k] = [] }, T::Hash[String, T::Array[String]])
@testing_formulae = T.let([], T::Array[String])
@tested_formulae = T.let([], T::Array[String])
end
protected
sig { returns(T.nilable(Pathname)) }
def cached_event_json
return unless (event_json = artifact_cache/"event.json").exist?
event_json
end
sig { returns(T.nilable(T::Hash[String, T.untyped])) }
def github_event_payload
return if (github_event_path = ENV.fetch("GITHUB_EVENT_PATH", nil)).blank?
JSON.parse(File.read(github_event_path))
end
sig { returns(T.nilable(String)) }
def previous_github_sha
return if tap.blank?
return unless repository.directory?
@@ -43,12 +60,22 @@ module Homebrew
# If we have a cached event payload, then we failed to get the artifact we wanted
# from `GITHUB_EVENT_PATH`, so use the cached payload to check for a SHA1.
event_payload = JSON.parse(cached_event_json.read) if cached_event_json.present?
event_payload = JSON.parse(T.must(cached_event_json).read) if cached_event_json.present?
event_payload ||= payload
event_payload.fetch("before", nil)
end
sig {
params(
check_suite_nodes: T::Array[T::Hash[String, T.untyped]],
repo: String,
event_name: String,
workflow_name: String,
check_run_name: String,
artifact_pattern: String,
).returns(T::Array[T::Hash[String, T.untyped]])
}
def artifact_metadata(check_suite_nodes, repo, event_name, workflow_name, check_run_name, artifact_pattern)
candidate_nodes = check_suite_nodes.select do |node|
next false if node.fetch("status") != "COMPLETED"
@@ -68,7 +95,7 @@ module Homebrew
return [] if candidate_nodes.blank?
run_id = candidate_nodes.max_by { |node| Time.parse(node.fetch("updatedAt")) }
.dig("workflowRun", "databaseId")
&.dig("workflowRun", "databaseId")
return [] if run_id.blank?
url = GitHub.url_to("repos", repo, "actions", "runs", run_id, "artifacts")
@@ -111,12 +138,13 @@ module Homebrew
}
GRAPHQL
sig { params(artifact_pattern: String, dry_run: T::Boolean).void }
def download_artifacts_from_previous_run!(artifact_pattern, dry_run:)
return if dry_run
return if GitHub::API.credentials_type == :none
return if (sha = previous_github_sha).blank?
pull_number = github_event_payload.dig("pull_request", "number")
pull_number = github_event_payload&.dig("pull_request", "number")
return if pull_number.blank?
github_repository = ENV.fetch("GITHUB_REPOSITORY")
@@ -147,7 +175,7 @@ module Homebrew
return if wanted_artifacts.empty?
if (attempted_artifact = wanted_artifacts.find do |artifact|
@downloaded_artifacts[sha].include?(artifact.fetch("name"))
@downloaded_artifacts.fetch(sha).include?(artifact.fetch("name"))
end)
opoo "Already tried #{attempted_artifact.fetch("name")} from #{sha}, giving up"
return
@@ -163,7 +191,7 @@ module Homebrew
wanted_artifacts.each do |artifact|
name = artifact.fetch("name")
ohai "Downloading artifact #{name} from #{sha}"
@downloaded_artifacts[sha] << name
@downloaded_artifacts.fetch(sha) << name
download_url = artifact.fetch("archive_download_url")
artifact_id = artifact.fetch("id")
@@ -180,30 +208,35 @@ module Homebrew
opoo e
end
sig { params(formula: Formula, git_ref: String).returns(T::Boolean) }
def no_diff?(formula, git_ref)
return false unless repository.directory?
@fetched_refs ||= []
@fetched_refs ||= T.let([], T.nilable(T::Array[String]))
if @fetched_refs.exclude?(git_ref)
test git, "-C", repository, "fetch", "origin", git_ref, ignore_failures: true
@fetched_refs << git_ref if steps.last.passed?
test git.to_s, "-C", repository.to_s, "fetch", "origin", git_ref, ignore_failures: true
@fetched_refs << git_ref if steps.fetch(-1).passed?
end
relative_formula_path = formula.path.relative_path_from(repository)
system(git, "-C", repository, "diff", "--no-ext-diff", "--quiet", git_ref, "--", relative_formula_path)
!!system(git.to_s, "-C", repository.to_s, "diff", "--no-ext-diff", "--quiet", git_ref, "--",
relative_formula_path.to_s)
end
sig { params(formula: String, bottle_dir: Pathname).returns(T.nilable(T::Hash[String, T.untyped])) }
def local_bottle_hash(formula, bottle_dir:)
return if (local_bottle_json = bottle_glob(formula, bottle_dir, ".json").first).blank?
JSON.parse(local_bottle_json.read)
end
sig { params(formula: Formula, formulae_dependents: T::Boolean).returns(T::Boolean) }
def artifact_cache_valid?(formula, formulae_dependents: false)
sha = if formulae_dependents
previous_github_sha
else
local_bottle_hash(formula, bottle_dir: artifact_cache)&.dig(formula.name, "formula", "tap_git_revision")
local_bottle_hash(formula.name, bottle_dir: artifact_cache)
&.dig(formula.name, "formula", "tap_git_revision")
end
return false if sha.blank?
@@ -222,10 +255,26 @@ module Homebrew
end
end
sig {
params(
formula_name: String,
bottle_dir: Pathname,
ext: String,
bottle_tag: String,
).returns(T::Array[Pathname])
}
def bottle_glob(formula_name, bottle_dir = Pathname.pwd, ext = ".tar.gz", bottle_tag: Utils::Bottles.tag.to_s)
bottle_dir.glob("#{formula_name}--*.#{bottle_tag}.bottle*#{ext}")
end
sig {
params(
formula_name: String,
testing_formulae_dependents: T::Boolean,
dry_run: T::Boolean,
bottle_dir: Pathname,
).returns(T::Boolean)
}
def install_formula_from_bottle!(formula_name, testing_formulae_dependents:, dry_run:,
bottle_dir: Pathname.pwd)
bottle_filename = bottle_glob(formula_name, bottle_dir).first
@@ -242,11 +291,11 @@ module Homebrew
install_args = []
install_args += %w[--ignore-dependencies --skip-post-install] if testing_formulae_dependents
test "brew", "install", *install_args, bottle_filename
install_step = steps.last
install_step = steps.fetch(-1)
if !dry_run && !testing_formulae_dependents && install_step.passed?
bottle_hash = local_bottle_hash(formula_name, bottle_dir:)
bottle_revision = bottle_hash.dig(formula_name, "formula", "tap_git_revision")
bottle_revision = bottle_hash&.dig(formula_name, "formula", "tap_git_revision")
bottle_header = "Bottle cache hit"
bottle_commit_details = if @fetched_refs&.include?(bottle_revision)
Utils.safe_popen_read(git, "-C", repository, "show", "--format=reference", bottle_revision)
@@ -259,7 +308,7 @@ module Homebrew
puts GitHub::Actions::Annotation.new(
:notice,
bottle_message,
file: bottle_hash.dig(formula_name, "formula", "tap_git_path"),
file: bottle_hash&.dig(formula_name, "formula", "tap_git_path"),
title: bottle_header,
)
else
@@ -274,6 +323,7 @@ module Homebrew
install_step.passed?
end
sig { params(formula: Formula, no_older_versions: T::Boolean).returns(T::Boolean) }
def bottled?(formula, no_older_versions: false)
# If a formula has an `:all` bottle, then all its dependencies have
# to be bottled too for us to use it. We only need to recurse
@@ -289,10 +339,18 @@ module Homebrew
end
end
sig {
params(
formula: Formula,
built_formulae: T::Enumerable[String],
no_older_versions: T::Boolean,
).returns(T::Boolean)
}
def bottled_or_built?(formula, built_formulae, no_older_versions: false)
bottled?(formula, no_older_versions:) || built_formulae.include?(formula.full_name)
end
sig { params(formula: Formula).returns(T::Boolean) }
def downloads_using_homebrew_curl?(formula)
[:stable, :head].any? do |spec_name|
next false unless (spec = formula.send(spec_name))
@@ -301,6 +359,7 @@ module Homebrew
end
end
sig { params(formula: Formula).void }
def install_curl_if_needed(formula)
return unless downloads_using_homebrew_curl?(formula)
@@ -308,6 +367,7 @@ module Homebrew
env: { "HOMEBREW_DEVELOPER" => nil }
end
sig { params(deps: T::Array[Dependency], reqs: T::Array[Requirement]).void }
def install_mercurial_if_needed(deps, reqs)
return if (deps | reqs).none? { |d| d.name == "mercurial" && d.build? }
@@ -315,6 +375,7 @@ module Homebrew
env: { "HOMEBREW_DEVELOPER" => nil }
end
sig { params(deps: T::Array[Dependency], reqs: T::Array[Requirement]).void }
def install_subversion_if_needed(deps, reqs)
return if (deps | reqs).none? { |d| d.name == "subversion" && d.build? }
@@ -322,6 +383,7 @@ module Homebrew
env: { "HOMEBREW_DEVELOPER" => nil }
end
sig { params(formula_name: String, reason: String).void }
def skipped(formula_name, reason)
@skipped_or_failed_formulae << formula_name
@@ -332,6 +394,7 @@ module Homebrew
opoo reason
end
sig { params(formula_name: String, reason: String).void }
def failed(formula_name, reason)
@skipped_or_failed_formulae << formula_name
@@ -342,6 +405,7 @@ module Homebrew
onoe reason
end
sig { params(formula: Formula).returns(T.nilable(String)) }
def unsatisfied_requirements_messages(formula)
f = Formulary.factory(formula.full_name)
fi = FormulaInstaller.new(f, build_bottle: true)
@@ -352,6 +416,7 @@ module Homebrew
unsatisfied_requirements.values.flatten.map(&:message).join("\n").presence
end
sig { params(keep_formulae: T::Array[String], args: Homebrew::Cmd::TestBotCmd::Args).void }
def cleanup_during!(keep_formulae = [], args:)
return unless cleanup?(args)
return unless HOMEBREW_CACHE.exist?
@@ -379,16 +444,20 @@ module Homebrew
end
if @cleaned_up_during.blank?
@cleaned_up_during = true
@cleaned_up_during = T.let(true, T.nilable(T::Boolean))
return
end
installed_formulae = Utils.safe_popen_read("brew", "list", "--full-name", "--formulae").split("\n")
uninstallable_formulae = installed_formulae - keep_formulae
@installed_formulae_deps ||= Hash.new do |h, formula|
h[formula] = Utils.safe_popen_read("brew", "deps", "--full-name", formula).split("\n")
end
@installed_formulae_deps ||= T.let(
Hash.new do |h, formula|
h[formula] = Utils.safe_popen_read("brew", "deps", "--full-name", formula).split("\n")
end,
T.nilable(T::Hash[String, T::Array[String]]),
)
uninstallable_formulae.reject! do |name|
keep_formulae.any? { |f| @installed_formulae_deps[f].include?(name) }
end
@@ -398,6 +467,7 @@ module Homebrew
test "brew", "uninstall", "--force", "--ignore-dependencies", *uninstallable_formulae
end
sig { returns(T::Array[String]) }
def sorted_formulae
changed_formulae_dependents = {}
@@ -1,9 +0,0 @@
# typed: strict
module Homebrew
module TestBot
class TestFormulae
include Kernel
end
end
end
+247 -213
View File
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "test_bot/junit"
@@ -17,238 +17,272 @@ require "test_bot/tap_syntax"
module Homebrew
module TestBot
module TestRunner
module_function
def ensure_blank_file_exists!(file)
if file.exist?
file.truncate(0)
else
FileUtils.touch(file)
end
end
def run!(tap, git:, args:)
tests = T.let([], T::Array[Test])
skip_setup = args.skip_setup?
skip_cleanup_before = T.let(false, T::Boolean)
bottle_output_path = Pathname.new("bottle_output.txt")
linkage_output_path = Pathname.new("linkage_output.txt")
@skipped_or_failed_formulae_output_path = Pathname.new("skipped_or_failed_formulae-#{Utils::Bottles.tag}.txt")
if no_only_args?(args) || args.only_formulae?
ensure_blank_file_exists!(bottle_output_path)
ensure_blank_file_exists!(linkage_output_path)
ensure_blank_file_exists!(@skipped_or_failed_formulae_output_path)
end
output_paths = {
bottle: bottle_output_path,
linkage: linkage_output_path,
skipped_or_failed_formulae: @skipped_or_failed_formulae_output_path,
TestRunnerTypes = T.type_alias do
{
setup: T.nilable(Setup),
tap_syntax: T.nilable(TapSyntax),
formulae_detect: T.nilable(FormulaeDetect),
formulae: T.nilable(Formulae),
formulae_dependents: T.nilable(FormulaeDependents),
cleanup_before: T.nilable(CleanupBefore),
cleanup_after: T.nilable(CleanupAfter),
bottles_fetch: T.nilable(BottlesFetch),
}
test_bot_args = args.named.dup
# With no arguments just build the most recent commit.
test_bot_args << "HEAD" if test_bot_args.empty?
test_bot_args.each do |argument|
skip_cleanup_after = argument != test_bot_args.last
current_tests = build_tests(argument, tap:,
git:,
output_paths:,
skip_setup:,
skip_cleanup_before:,
skip_cleanup_after:,
args:)
skip_setup = true
skip_cleanup_before = true
tests += current_tests.values
run_tests(current_tests, args:)
end
failed_steps = tests.map(&:failed_steps)
.flatten
.compact
ignored_steps = tests.map(&:ignored_steps)
.flatten
.compact
steps_output = if failed_steps.blank? && ignored_steps.blank?
"All steps passed!"
else
output_lines = []
if ignored_steps.present?
output_lines += ["Warning: #{ignored_steps.count} failed step#{"s" if ignored_steps.count > 1} ignored!"]
output_lines += ignored_steps.map(&:command_trimmed)
end
if failed_steps.present?
output_lines += ["Error: #{failed_steps.count} failed step#{"s" if failed_steps.count > 1}!"]
output_lines += failed_steps.map(&:command_trimmed)
end
output_lines.join("\n")
end
puts steps_output
steps_output_path = Pathname.new("steps_output.txt")
steps_output_path.unlink if steps_output_path.exist?
steps_output_path.write(steps_output)
if args.junit? && (no_only_args?(args) || args.only_formulae? || args.only_formulae_dependents?)
junit_filters = %w[audit test]
junit = Junit.new(tests)
junit.build(filters: junit_filters)
junit.write("brew-test-bot.xml")
end
failed_steps.empty?
end
def no_only_args?(args)
any_only = args.only_cleanup_before? ||
args.only_setup? ||
args.only_tap_syntax? ||
args.only_formulae? ||
args.only_formulae_detect? ||
args.only_formulae_dependents? ||
args.only_bottles_fetch? ||
args.only_cleanup_after?
!any_only
end
class << self
sig { params(tap: T.nilable(Tap), git: String, args: Homebrew::Cmd::TestBotCmd::Args).returns(T::Boolean) }
def run!(tap, git:, args:)
tests = T.let([], T::Array[Test])
skip_setup = args.skip_setup?
skip_cleanup_before = T.let(false, T::Boolean)
def build_tests(argument, tap:, git:, output_paths:, skip_setup:,
skip_cleanup_before:, skip_cleanup_after:, args:)
tests = {}
bottle_output_path = Pathname.new("bottle_output.txt")
linkage_output_path = Pathname.new("linkage_output.txt")
skipped_or_failed_formulae_output_path = Pathname.new("skipped_or_failed_formulae-#{Utils::Bottles.tag}.txt")
@skipped_or_failed_formulae_output_path = T.let(skipped_or_failed_formulae_output_path,
T.nilable(Pathname))
no_only_args = no_only_args?(args)
if no_only_args?(args) || args.only_formulae?
ensure_blank_file_exists!(bottle_output_path)
ensure_blank_file_exists!(linkage_output_path)
ensure_blank_file_exists!(skipped_or_failed_formulae_output_path)
end
if !skip_setup && (no_only_args || args.only_setup?)
tests[:setup] = Setup.new(dry_run: args.dry_run?,
fail_fast: args.fail_fast?,
verbose: args.verbose?)
output_paths = {
bottle: bottle_output_path,
linkage: linkage_output_path,
skipped_or_failed_formulae: skipped_or_failed_formulae_output_path,
}
test_bot_args = args.named.dup
# With no arguments just build the most recent commit.
test_bot_args << "HEAD" if test_bot_args.empty?
test_bot_args.each do |argument|
skip_cleanup_after = argument != test_bot_args.last
current_tests = build_tests(argument, tap:,
git:,
output_paths:,
skip_setup:,
skip_cleanup_before:,
skip_cleanup_after:,
args:)
skip_setup = true
skip_cleanup_before = true
tests += current_tests.values.compact
run_tests(current_tests, args:)
end
failed_steps = tests.map(&:failed_steps)
.flatten
.compact
ignored_steps = tests.map(&:ignored_steps)
.flatten
.compact
steps_output = if failed_steps.blank? && ignored_steps.blank?
"All steps passed!"
else
output_lines = []
if ignored_steps.present?
output_lines += [
"Warning: #{ignored_steps.count} failed step#{"s" if ignored_steps.count > 1} ignored!",
]
output_lines += ignored_steps.map(&:command_trimmed)
end
if failed_steps.present?
output_lines += ["Error: #{failed_steps.count} failed step#{"s" if failed_steps.count > 1}!"]
output_lines += failed_steps.map(&:command_trimmed)
end
output_lines.join("\n")
end
puts steps_output
steps_output_path = Pathname.new("steps_output.txt")
steps_output_path.unlink if steps_output_path.exist?
steps_output_path.write(steps_output)
if args.junit? && (no_only_args?(args) || args.only_formulae? || args.only_formulae_dependents?)
junit_filters = %w[audit test]
junit = Junit.new(tests)
junit.build(filters: junit_filters)
junit.write("brew-test-bot.xml")
end
failed_steps.empty?
end
if no_only_args || args.only_tap_syntax?
tests[:tap_syntax] = TapSyntax.new(tap: tap || CoreTap.instance,
dry_run: args.dry_run?,
private
sig { params(file: Pathname).void }
def ensure_blank_file_exists!(file)
if file.exist?
file.truncate(0)
else
FileUtils.touch(file)
end
end
sig { params(args: Homebrew::Cmd::TestBotCmd::Args).returns(T::Boolean) }
def no_only_args?(args)
any_only = args.only_cleanup_before? ||
args.only_setup? ||
args.only_tap_syntax? ||
args.only_formulae? ||
args.only_formulae_detect? ||
args.only_formulae_dependents? ||
args.only_bottles_fetch? ||
args.only_cleanup_after?
!any_only
end
sig {
params(
argument: String,
tap: T.nilable(Tap),
git: String,
output_paths: T::Hash[Symbol, Pathname],
skip_setup: T::Boolean,
skip_cleanup_before: T::Boolean,
skip_cleanup_after: T::Boolean,
args: Homebrew::Cmd::TestBotCmd::Args,
).returns(TestRunnerTypes)
}
def build_tests(argument, tap:, git:, output_paths:, skip_setup:,
skip_cleanup_before:, skip_cleanup_after:, args:)
no_only_args = no_only_args?(args)
if !skip_setup && (no_only_args || args.only_setup?)
setup = Setup.new(dry_run: args.dry_run?,
fail_fast: args.fail_fast?,
verbose: args.verbose?)
end
if no_only_args || args.only_tap_syntax?
tap_syntax = TapSyntax.new(tap: tap || CoreTap.instance,
dry_run: args.dry_run?,
git:,
fail_fast: args.fail_fast?,
verbose: args.verbose?)
end
no_formulae_flags = args.testing_formulae.nil? &&
args.added_formulae.nil? &&
args.deleted_formulae.nil?
if no_formulae_flags && (no_only_args || args.only_formulae? || args.only_formulae_detect?)
formulae_detect = FormulaeDetect.new(argument, tap:,
git:,
dry_run: args.dry_run?,
fail_fast: args.fail_fast?,
verbose: args.verbose?)
end
if no_only_args || args.only_formulae?
formulae = Formulae.new(tap:,
git:,
dry_run: args.dry_run?,
fail_fast: args.fail_fast?,
verbose: args.verbose?,
output_paths:)
end
if !args.skip_dependents? && (no_only_args || args.only_formulae? || args.only_formulae_dependents?)
formulae_dependents = FormulaeDependents.new(tap:,
git:,
dry_run: args.dry_run?,
fail_fast: args.fail_fast?,
verbose: args.verbose?)
end
if Homebrew::TestBot.cleanup?(args)
if !skip_cleanup_before && (no_only_args || args.only_cleanup_before?)
cleanup_before = CleanupBefore.new(tap:,
git:,
dry_run: args.dry_run?,
fail_fast: args.fail_fast?,
verbose: args.verbose?)
end
if !skip_cleanup_after && (no_only_args || args.only_cleanup_after?)
cleanup_after = CleanupAfter.new(tap:,
git:,
dry_run: args.dry_run?,
fail_fast: args.fail_fast?,
verbose: args.verbose?)
end
end
if args.only_bottles_fetch?
bottles_fetch = BottlesFetch.new(tap:,
git:,
dry_run: args.dry_run?,
fail_fast: args.fail_fast?,
verbose: args.verbose?)
end
no_formulae_flags = args.testing_formulae.nil? &&
args.added_formulae.nil? &&
args.deleted_formulae.nil?
if no_formulae_flags && (no_only_args || args.only_formulae? || args.only_formulae_detect?)
tests[:formulae_detect] = FormulaeDetect.new(argument, tap:,
git:,
dry_run: args.dry_run?,
fail_fast: args.fail_fast?,
verbose: args.verbose?)
end
if no_only_args || args.only_formulae?
tests[:formulae] = Formulae.new(tap:,
git:,
dry_run: args.dry_run?,
fail_fast: args.fail_fast?,
verbose: args.verbose?,
output_paths:)
end
if !args.skip_dependents? && (no_only_args || args.only_formulae? || args.only_formulae_dependents?)
tests[:formulae_dependents] = FormulaeDependents.new(tap:,
git:,
dry_run: args.dry_run?,
fail_fast: args.fail_fast?,
verbose: args.verbose?)
end
if Homebrew::TestBot.cleanup?(args)
if !skip_cleanup_before && (no_only_args || args.only_cleanup_before?)
tests[:cleanup_before] = CleanupBefore.new(tap:,
git:,
dry_run: args.dry_run?,
fail_fast: args.fail_fast?,
verbose: args.verbose?)
end
if !skip_cleanup_after && (no_only_args || args.only_cleanup_after?)
tests[:cleanup_after] = CleanupAfter.new(tap:,
git:,
dry_run: args.dry_run?,
fail_fast: args.fail_fast?,
verbose: args.verbose?)
end
{ setup:, tap_syntax:, formulae_detect:, formulae:, formulae_dependents:,
cleanup_before:, cleanup_after:, bottles_fetch: }
end
if args.only_bottles_fetch?
tests[:bottles_fetch] = BottlesFetch.new(tap:,
git:,
dry_run: args.dry_run?,
fail_fast: args.fail_fast?,
verbose: args.verbose?)
end
sig { params(tests: TestRunnerTypes, args: Homebrew::Cmd::TestBotCmd::Args).void }
def run_tests(tests, args:)
tests[:cleanup_before]&.run!(args:)
begin
tests[:setup]&.run!(args:)
tests[:tap_syntax]&.run!(args:)
tests
end
testing_formulae, added_formulae, deleted_formulae = if (detect_test = tests[:formulae_detect])
detect_test.run!(args:)
def run_tests(tests, args:)
tests[:cleanup_before]&.run!(args:)
begin
tests[:setup]&.run!(args:)
tests[:tap_syntax]&.run!(args:)
[
detect_test.testing_formulae,
detect_test.added_formulae,
detect_test.deleted_formulae,
]
else
[
args.testing_formulae.to_a,
args.added_formulae.to_a,
args.deleted_formulae.to_a,
]
end
testing_formulae, added_formulae, deleted_formulae = if (detect_test = tests[:formulae_detect])
detect_test.run!(args:)
skipped_or_failed_formulae = if (formulae_test = tests[:formulae])
formulae_test.testing_formulae = testing_formulae
formulae_test.added_formulae = added_formulae
formulae_test.deleted_formulae = deleted_formulae
[
detect_test.testing_formulae,
detect_test.added_formulae,
detect_test.deleted_formulae,
]
else
[
args.testing_formulae.to_a,
args.added_formulae.to_a,
args.deleted_formulae.to_a,
]
formulae_test.run!(args:)
formulae_test.skipped_or_failed_formulae
elsif args.skipped_or_failed_formulae.present?
Array.new(T.must(args.skipped_or_failed_formulae))
elsif T.must(@skipped_or_failed_formulae_output_path).exist?
T.must(@skipped_or_failed_formulae_output_path).read.chomp.split(",")
else
[]
end
if (dependents_test = tests[:formulae_dependents])
dependents_test.testing_formulae = testing_formulae
dependents_test.skipped_or_failed_formulae = skipped_or_failed_formulae
dependents_test.tested_formulae = args.tested_formulae.to_a.presence || testing_formulae
dependents_test.run!(args:)
end
if (fetch_test = tests[:bottles_fetch])
fetch_test.testing_formulae = testing_formulae
fetch_test.run!(args:)
end
ensure
tests[:cleanup_after]&.run!(args:)
end
skipped_or_failed_formulae = if (formulae_test = tests[:formulae])
formulae_test.testing_formulae = testing_formulae
formulae_test.added_formulae = added_formulae
formulae_test.deleted_formulae = deleted_formulae
formulae_test.run!(args:)
formulae_test.skipped_or_failed_formulae
elsif args.skipped_or_failed_formulae.present?
Array.new(args.skipped_or_failed_formulae)
elsif @skipped_or_failed_formulae_output_path.exist?
@skipped_or_failed_formulae_output_path.read.chomp.split(",")
else
[]
end
if (dependents_test = tests[:formulae_dependents])
dependents_test.testing_formulae = testing_formulae
dependents_test.skipped_or_failed_formulae = skipped_or_failed_formulae
dependents_test.tested_formulae = args.tested_formulae.to_a.presence || testing_formulae
dependents_test.run!(args:)
end
if (fetch_test = tests[:bottles_fetch])
fetch_test.testing_formulae = testing_formulae
fetch_test.run!(args:)
end
ensure
tests[:cleanup_after]&.run!(args:)
end
end
end
@@ -1,9 +0,0 @@
# typed: strict
module Homebrew
module TestBot
module TestRunner
include Kernel
end
end
end