diff --git a/Library/Homebrew/dev-cmd/determine-test-runners.rb b/Library/Homebrew/dev-cmd/determine-test-runners.rb index d75f5e6e7a..b015e06e21 100755 --- a/Library/Homebrew/dev-cmd/determine-test-runners.rb +++ b/Library/Homebrew/dev-cmd/determine-test-runners.rb @@ -2,6 +2,59 @@ # frozen_string_literal: true require "cli/parser" +require "formula" + +class TestRunnerFormula + extend T::Sig + + sig { returns(String) } + attr_reader :name + + sig { returns(Formula) } + attr_reader :formula + + sig { params(name: String).void } + def initialize(name) + @name = T.let(name, String) + @formula = T.let(Formula[name], Formula) + @dependent_hash = T.let({}, T::Hash[T::Boolean, T::Array[TestRunnerFormula]]) + freeze + end + + sig { returns(T::Boolean) } + def macos_only? + formula.requirements.any? { |r| r.is_a?(MacOSRequirement) && !r.version_specified? } + end + + sig { returns(T::Boolean) } + def linux_only? + formula.requirements.any?(LinuxRequirement) + end + + sig { returns(T::Boolean) } + def x86_64_only? + formula.requirements.any? { |r| r.is_a?(ArchRequirement) && (r.arch == :x86_64) } + end + + sig { returns(T::Boolean) } + def arm64_only? + formula.requirements.any? { |r| r.is_a?(ArchRequirement) && (r.arch == :arm64) } + end + + sig { returns(T.nilable(MacOSRequirement)) } + def versioned_macos_requirement + formula.requirements.find { |r| r.is_a?(MacOSRequirement) && r.version_specified? } + end + + sig { params(macos_version: MacOS::Version).returns(T::Boolean) } + def compatible_with?(macos_version) + # Assign to a variable to assist type-checking. + requirement = versioned_macos_requirement + return true if requirement.blank? + + macos_version.public_send(requirement.comparator, requirement.version) + end +end module Homebrew extend T::Sig @@ -15,7 +68,7 @@ module Homebrew Determines the runners used to test formulae or their dependents. EOS switch "--dependents", - description: "Determine runners for testing dependents." + description: "Determine runners for testing dependents. (requires Linux)" named_args min: 1, max: 2 @@ -23,9 +76,158 @@ module Homebrew end end + sig { + params( + _testing_formulae: T::Array[TestRunnerFormula], + reject_platform: T.nilable(Symbol), + reject_arch: T.nilable(Symbol), + select_macos_version: T.nilable(MacOS::Version), + ).void + } + def self.formulae_have_untested_dependents?(_testing_formulae, reject_platform:, + reject_arch:, select_macos_version:) + odie "`--dependents` is supported only on Linux!" + end + + sig { + params( + formulae: T::Array[TestRunnerFormula], + dependents: T::Boolean, + deleted_formulae: T.nilable(T::Array[String]), + reject_platform: T.nilable(Symbol), + reject_arch: T.nilable(Symbol), + select_macos_version: T.nilable(MacOS::Version), + ).returns(T::Boolean) + } + def self.add_runner?(formulae, + dependents:, + deleted_formulae:, + reject_platform: nil, + reject_arch: nil, + select_macos_version: nil) + if dependents + formulae_have_untested_dependents?( + formulae, + reject_platform: reject_platform, + reject_arch: reject_arch, + select_macos_version: select_macos_version, + ) + else + return true if deleted_formulae.present? + + compatible_formulae = formulae.dup + + compatible_formulae.reject! { |formula| formula.send(:"#{reject_platform}_only?") } if reject_platform + compatible_formulae.reject! { |formula| formula.send(:"#{reject_arch}_only?") } if reject_arch + compatible_formulae.select! { |formula| formula.compatible_with?(select_macos_version) } if select_macos_version + + compatible_formulae.present? + end + end + sig { void } def self.determine_test_runners - odie "This command is supported only on Linux!" + args = determine_test_runners_args.parse + testing_formulae = args.named.first.split(",") + testing_formulae.map! { |name| TestRunnerFormula.new(name) } + .freeze + deleted_formulae = args.named.second&.split(",") + + runners = [] + + linux_runner = ENV.fetch("HOMEBREW_LINUX_RUNNER") { raise "HOMEBREW_LINUX_RUNNER is not defined" } + linux_cleanup = ENV.fetch("HOMEBREW_LINUX_CLEANUP") { raise "HOMEBREW_LINUX_CLEANUP is not defined" } + + linux_runner_spec = { + runner: linux_runner, + container: { + image: "ghcr.io/homebrew/ubuntu22.04:master", + options: "--user=linuxbrew -e GITHUB_ACTIONS_HOMEBREW_SELF_HOSTED", + }, + workdir: "/github/home", + timeout: 4320, + cleanup: linux_cleanup == "true", + } + + with_env(HOMEBREW_SIMULATE_MACOS_ON_LINUX: nil) do + if add_runner?( + testing_formulae, + reject_platform: :macos, + reject_arch: :arm64, + deleted_formulae: deleted_formulae, + dependents: args.dependents?, + ) + runners << linux_runner_spec + end + end + + # TODO: `HOMEBREW_SIMULATE_MACOS_ON_LINUX` simulates the oldest version of macOS. + # Handle formulae that are dependents only on new versions of macOS. + with_env(HOMEBREW_SIMULATE_MACOS_ON_LINUX: "1") do + if add_runner?( + testing_formulae, + reject_platform: :linux, + deleted_formulae: deleted_formulae, + dependents: args.dependents?, + ) + add_intel_runners = add_runner?( + testing_formulae, + reject_platform: :linux, + reject_arch: :arm64, + deleted_formulae: deleted_formulae, + dependents: args.dependents?, + ) + add_m1_runners = add_runner?( + testing_formulae, + reject_platform: :linux, + reject_arch: :x86_64, + deleted_formulae: deleted_formulae, + dependents: args.dependents?, + ) + + github_run_id = ENV.fetch("GITHUB_RUN_ID") { raise "GITHUB_RUN_ID is not defined" } + github_run_attempt = ENV.fetch("GITHUB_RUN_ATTEMPT") { raise "GITHUB_RUN_ATTEMPT is not defined" } + + MacOSVersions::SYMBOLS.each_value do |version| + macos_version = MacOS::Version.new(version) + next if macos_version.outdated_release? || macos_version.prerelease? + + unless add_runner?( + testing_formulae, + reject_platform: :linux, + select_macos_version: macos_version, + deleted_formulae: deleted_formulae, + dependents: args.dependents?, + ) + next # No formulae to test on this macOS version. + end + + ephemeral_suffix = "-#{github_run_id}-#{github_run_attempt}" + runners << { runner: "#{macos_version}#{ephemeral_suffix}", cleanup: false } if add_intel_runners + + next unless add_m1_runners + + # Use bare metal runner when testing dependents on Monterey. + if macos_version >= :ventura || (macos_version >= :monterey && !args.dependents?) + runners << { runner: "#{macos_version}-arm64#{ephemeral_suffix}", cleanup: false } + elsif macos_version >= :big_sur + runners << { runner: "#{macos_version}-arm64", cleanup: true } + end + end + end + end + + if !args.dependents? && runners.blank? + # If there are no tests to run, add a runner that is meant to do nothing + # to support making the `tests` job a required status check. + runners << { runner: "ubuntu-latest", no_op: true } + end + + github_output = ENV.fetch("GITHUB_OUTPUT") { raise "GITHUB_OUTPUT is not defined" } + File.open(github_output, "a") do |f| + f.puts("runners=#{runners.to_json}") + f.puts("runners_present=#{runners.present?}") + end end end diff --git a/Library/Homebrew/extend/os/linux/dev-cmd/determine-test-runners.rb b/Library/Homebrew/extend/os/linux/dev-cmd/determine-test-runners.rb index f77bf20fd6..2e2828e5de 100755 --- a/Library/Homebrew/extend/os/linux/dev-cmd/determine-test-runners.rb +++ b/Library/Homebrew/extend/os/linux/dev-cmd/determine-test-runners.rb @@ -1,59 +1,9 @@ # typed: strict # frozen_string_literal: true -require "formula" - class TestRunnerFormula extend T::Sig - sig { returns(String) } - attr_reader :name - - sig { returns(Formula) } - attr_reader :formula - - sig { params(name: String).void } - def initialize(name) - @name = T.let(name, String) - @formula = T.let(Formula[name], Formula) - @dependent_hash = T.let({}, T::Hash[T::Boolean, T::Array[TestRunnerFormula]]) - freeze - end - - sig { returns(T::Boolean) } - def macos_only? - formula.requirements.any? { |r| r.is_a?(MacOSRequirement) && !r.version_specified? } - end - - sig { returns(T::Boolean) } - def linux_only? - formula.requirements.any?(LinuxRequirement) - end - - sig { returns(T::Boolean) } - def x86_64_only? - formula.requirements.any? { |r| r.is_a?(ArchRequirement) && (r.arch == :x86_64) } - end - - sig { returns(T::Boolean) } - def arm64_only? - formula.requirements.any? { |r| r.is_a?(ArchRequirement) && (r.arch == :arm64) } - end - - sig { returns(T.nilable(MacOSRequirement)) } - def versioned_macos_requirement - formula.requirements.find { |r| r.is_a?(MacOSRequirement) && r.version_specified? } - end - - sig { params(macos_version: MacOS::Version).returns(T::Boolean) } - def compatible_with?(macos_version) - # Assign to a variable to assist type-checking. - requirement = versioned_macos_requirement - return true if requirement.blank? - - macos_version.public_send(requirement.comparator, requirement.version) - end - sig { returns(T::Array[TestRunnerFormula]) } def dependents @dependent_hash[ENV["HOMEBREW_SIMULATE_MACOS_ON_LINUX"].present?] ||= with_env(HOMEBREW_STDERR: "1") do @@ -87,12 +37,8 @@ module Homebrew compatible_dependents = formula.dependents.dup - compatible_dependents.reject! { |dependent_f| dependent_f.send(:"#{reject_arch}_only?")} if reject_arch - - if reject_platform - compatible_dependents.reject! { |dependent_f| dependent_f.send(:"#{reject_platform}_only?") } - end - + compatible_dependents.reject! { |dependent_f| dependent_f.send(:"#{reject_arch}_only?") } if reject_arch + compatible_dependents.reject! { |dependent_f| dependent_f.send(:"#{reject_platform}_only?") } if reject_platform if select_macos_version compatible_dependents.select! { |dependent_f| dependent_f.compatible_with?(select_macos_version) } end @@ -100,145 +46,4 @@ module Homebrew (compatible_dependents - testing_formulae).present? end end - - sig { - params( - formulae: T::Array[TestRunnerFormula], - dependents: T::Boolean, - deleted_formulae: T.nilable(T::Array[String]), - reject_platform: T.nilable(Symbol), - reject_arch: T.nilable(Symbol), - select_macos_version: T.nilable(MacOS::Version), - ).returns(T::Boolean) - } - def self.add_runner?(formulae, - dependents:, - deleted_formulae:, - reject_platform: nil, - reject_arch: nil, - select_macos_version: nil) - if dependents - formulae_have_untested_dependents?( - formulae, - reject_platform: reject_platform, - reject_arch: reject_arch, - select_macos_version: select_macos_version, - ) - else - return true if deleted_formulae.present? - - compatible_formulae = formulae.dup - - compatible_formulae.reject! { |formula| formula.send(:"#{reject_platform}_only?") } if reject_platform - compatible_formulae.reject! { |formula| formula.send(:"#{reject_arch}_only?") } if reject_arch - compatible_formulae.select! { |formula| formula.compatible_with?(select_macos_version) } if select_macos_version - - compatible_formulae.present? - end - end - - sig { void } - def self.determine_test_runners - args = determine_test_runners_args.parse - testing_formulae = args.named.first.split(",") - testing_formulae.map! { |name| TestRunnerFormula.new(name) } - .freeze - deleted_formulae = args.named.second&.split(",") - - runners = [] - - linux_runner = ENV.fetch("HOMEBREW_LINUX_RUNNER") { raise "HOMEBREW_LINUX_RUNNER is not defined" } - linux_cleanup = ENV.fetch("HOMEBREW_LINUX_CLEANUP") { raise "HOMEBREW_LINUX_CLEANUP is not defined" } - - linux_runner_spec = { - runner: linux_runner, - container: { - image: "ghcr.io/homebrew/ubuntu22.04:master", - options: "--user=linuxbrew -e GITHUB_ACTIONS_HOMEBREW_SELF_HOSTED", - }, - workdir: "/github/home", - timeout: 4320, - cleanup: linux_cleanup == "true", - } - - with_env(HOMEBREW_SIMULATE_MACOS_ON_LINUX: nil) do - if add_runner?( - testing_formulae, - reject_platform: :macos, - reject_arch: :arm64, - deleted_formulae: deleted_formulae, - dependents: args.dependents?, - ) - runners << linux_runner_spec - end - end - - # TODO: `HOMEBREW_SIMULATE_MACOS_ON_LINUX` simulates the oldest version of macOS. - # Handle formulae that are dependents only on new versions of macOS. - with_env(HOMEBREW_SIMULATE_MACOS_ON_LINUX: "1") do - if add_runner?( - testing_formulae, - reject_platform: :linux, - deleted_formulae: deleted_formulae, - dependents: args.dependents?, - ) - add_intel_runners = add_runner?( - testing_formulae, - reject_platform: :linux, - reject_arch: :arm64, - deleted_formulae: deleted_formulae, - dependents: args.dependents?, - ) - add_m1_runners = add_runner?( - testing_formulae, - reject_platform: :linux, - reject_arch: :x86_64, - deleted_formulae: deleted_formulae, - dependents: args.dependents?, - ) - - github_run_id = ENV.fetch("GITHUB_RUN_ID") { raise "GITHUB_RUN_ID is not defined" } - github_run_attempt = ENV.fetch("GITHUB_RUN_ATTEMPT") { raise "GITHUB_RUN_ATTEMPT is not defined" } - - MacOSVersions::SYMBOLS.each_value do |version| - macos_version = MacOS::Version.new(version) - next if macos_version.outdated_release? || macos_version.prerelease? - - unless add_runner?( - testing_formulae, - reject_platform: :linux, - select_macos_version: macos_version, - deleted_formulae: deleted_formulae, - dependents: args.dependents?, - ) - next # No formulae to test on this macOS version. - end - - ephemeral_suffix = "-#{github_run_id}-#{github_run_attempt}" - runners << { runner: "#{macos_version}#{ephemeral_suffix}", cleanup: false } if add_intel_runners - - next unless add_m1_runners - - # Use bare metal runner when testing dependents on Monterey. - if macos_version >= :ventura || (macos_version >= :monterey && !args.dependents?) - runners << { runner: "#{macos_version}-arm64#{ephemeral_suffix}", cleanup: false } - elsif macos_version >= :big_sur - runners << { runner: "#{macos_version}-arm64", cleanup: true } - end - end - end - end - - if !args.dependents? && runners.blank? - # If there are no tests to run, add a runner that is meant to do nothing - # to support making the `tests` job a required status check. - runners << { runner: "ubuntu-latest", no_op: true } - end - - github_output = ENV.fetch("GITHUB_OUTPUT") { raise "GITHUB_OUTPUT is not defined" } - File.open(github_output, "a") do |f| - f.puts("runners=#{runners.to_json}") - f.puts("runners_present=#{runners.present?}") - end - end end diff --git a/Library/Homebrew/test/dev-cmd/determine-test-runners_spec.rb b/Library/Homebrew/test/dev-cmd/determine-test-runners_spec.rb index d26c01b159..0aa6ee8927 100644 --- a/Library/Homebrew/test/dev-cmd/determine-test-runners_spec.rb +++ b/Library/Homebrew/test/dev-cmd/determine-test-runners_spec.rb @@ -35,7 +35,7 @@ describe "brew determine-test-runners" do .and be_a_failure end - it "fails when the necessary environment variables are missing", :integration_test, :needs_linux do + it "fails when the necessary environment variables are missing", :integration_test do setup_test_formula "testball" runner_env.each_key do |k| @@ -49,7 +49,7 @@ describe "brew determine-test-runners" do end end - it "assigns all runners for formulae without any requirements", :integration_test, :needs_linux do + it "assigns all runners for formulae without any requirements", :integration_test do setup_test_formula "testball" expect { brew "determine-test-runners", "testball", runner_env.merge({ "GITHUB_OUTPUT" => github_output }) } @@ -61,7 +61,7 @@ describe "brew determine-test-runners" do expect(get_runners(github_output)).to eq(all_runners) end - it "assigns all runners when there are deleted formulae", :integration_test, :needs_linux do + it "assigns all runners when there are deleted formulae", :integration_test do expect { brew "determine-test-runners", "", "testball", runner_env.merge({ "GITHUB_OUTPUT" => github_output }) } .to not_to_output.to_stdout .and not_to_output.to_stderr @@ -71,8 +71,7 @@ describe "brew determine-test-runners" do expect(get_runners(github_output)).to eq(all_runners) end - it "assigns `ubuntu-latest` when there are no testing formulae and no deleted formulae", :integration_test, - :needs_linux do + it "assigns `ubuntu-latest` when there are no testing formulae and no deleted formulae", :integration_test do expect { brew "determine-test-runners", "", runner_env.merge({ "GITHUB_OUTPUT" => github_output }) } .to not_to_output.to_stdout .and not_to_output.to_stderr @@ -82,7 +81,7 @@ describe "brew determine-test-runners" do expect(get_runners(github_output)).to eq(["ubuntu-latest"]) end - it "assigns only Intel runners when a formula `depends_on arch: :x86_64`", :integration_test, :needs_linux do + it "assigns only Intel runners when a formula `depends_on arch: :x86_64`", :integration_test do setup_test_formula "intel_depender", <<~RUBY url "https://brew.sh/intel_depender-1.0.tar.gz" depends_on arch: :x86_64 @@ -97,7 +96,7 @@ describe "brew determine-test-runners" do expect(get_runners(github_output)).to eq(intel_runners) end - it "assigns only ARM64 runners when a formula `depends_on arch: :arm64`", :integration_test, :needs_linux do + it "assigns only ARM64 runners when a formula `depends_on arch: :arm64`", :integration_test do setup_test_formula "fancy-m1-ml-framework", <<~RUBY url "https://brew.sh/fancy-m1-ml-framework-1.0.tar.gz" depends_on arch: :arm64 @@ -114,7 +113,7 @@ describe "brew determine-test-runners" do expect(get_runners(github_output)).to eq(arm64_runners) end - it "assigns only macOS runners when a formula `depends_on :macos`", :integration_test, :needs_linux do + it "assigns only macOS runners when a formula `depends_on :macos`", :integration_test do setup_test_formula "xcode-helper", <<~RUBY url "https://brew.sh/xcode-helper-1.0.tar.gz" depends_on :macos @@ -129,7 +128,7 @@ describe "brew determine-test-runners" do expect(get_runners(github_output)).to eq(macos_runners) end - it "assigns only Linux runners when a formula `depends_on :linux`", :integration_test, :needs_linux do + it "assigns only Linux runners when a formula `depends_on :linux`", :integration_test do setup_test_formula "linux-kernel-requirer", <<~RUBY url "https://brew.sh/linux-kernel-requirer-1.0.tar.gz" depends_on :linux @@ -147,7 +146,7 @@ describe "brew determine-test-runners" do end # TODO: Keep this updated to use the newest supported macOS version. - it "assigns only compatible runners when there is a versioned macOS requirement", :integration_test, :needs_linux do + it "assigns only compatible runners when there is a versioned macOS requirement", :integration_test do setup_test_formula "needs-macos-13", <<~RUBY url "https://brew.sh/needs-macos-13-1.0.tar.gz" depends_on macos: :ventura @@ -162,6 +161,17 @@ describe "brew determine-test-runners" do expect(get_runners(github_output)).to eq(["13", "13-arm64", linux_runner]) expect(get_runners(github_output)).not_to eq(all_runners) end + + describe "--dependents" do + it "fails on macOS", :integration_test, :needs_macos do + setup_test_formula "testball" + + expect { brew "determine-test-runners", "--dependents", "testball", runner_env.dup } + .to not_to_output.to_stdout + .and output("Error: `--dependents` is supported only on Linux!\n").to_stderr + .and be_a_failure + end + end end def get_runners(file)