diff --git a/Library/Homebrew/dev-cmd/determine-test-runners.rb b/Library/Homebrew/dev-cmd/determine-test-runners.rb new file mode 100755 index 0000000000..c3903988fe --- /dev/null +++ b/Library/Homebrew/dev-cmd/determine-test-runners.rb @@ -0,0 +1,53 @@ +# typed: strict +# frozen_string_literal: true + +require "cli/parser" +require "test_runner_formula" +require "github_runner_matrix" + +module Homebrew + extend T::Sig + + sig { returns(Homebrew::CLI::Parser) } + def self.determine_test_runners_args + Homebrew::CLI::Parser.new do + usage_banner <<~EOS + `determine-test-runners` [] + + Determines the runners used to test formulae or their dependents. + EOS + switch "--eval-all", + description: "Evaluate all available formulae, whether installed or not, to determine testing " \ + "dependents." + switch "--dependents", + description: "Determine runners for testing dependents. Requires `--eval-all` or `HOMEBREW_EVAL_ALL`." + + named_args min: 1, max: 2 + + hide_from_man_page! + end + end + + sig { void } + def self.determine_test_runners + args = determine_test_runners_args.parse + + eval_all = args.eval_all? || Homebrew::EnvConfig.eval_all? + + odie "`--dependents` requires `--eval-all` or `HOMEBREW_EVAL_ALL`!" if args.dependents? && !eval_all + + testing_formulae = args.named.first.split(",") + testing_formulae.map! { |name| TestRunnerFormula.new(Formulary.factory(name), eval_all: eval_all) } + .freeze + deleted_formulae = args.named.second&.split(",").freeze + + runner_matrix = GitHubRunnerMatrix.new(testing_formulae, deleted_formulae, dependent_matrix: args.dependents?) + runners = runner_matrix.active_runner_specs_hash + + github_output = ENV.fetch("GITHUB_OUTPUT") + 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/on_system.rb b/Library/Homebrew/extend/on_system.rb index 5d4c581f44..2fc253904c 100644 --- a/Library/Homebrew/extend/on_system.rb +++ b/Library/Homebrew/extend/on_system.rb @@ -28,13 +28,13 @@ module OnSystem return false if Homebrew::SimulateSystem.simulating_or_running_on_linux? - base_os = MacOS::Version.from_symbol(os_name) + base_os = OS::Mac::Version.from_symbol(os_name) current_os = if Homebrew::SimulateSystem.current_os == :macos # Assume the oldest macOS version when simulating a generic macOS version # Version::NULL is always treated as less than any other version. Version::NULL else - MacOS::Version.from_symbol(Homebrew::SimulateSystem.current_os) + OS::Mac::Version.from_symbol(Homebrew::SimulateSystem.current_os) end return current_os >= base_os if or_condition == :or_newer diff --git a/Library/Homebrew/github_runner.rb b/Library/Homebrew/github_runner.rb new file mode 100644 index 0000000000..0dbf845cdc --- /dev/null +++ b/Library/Homebrew/github_runner.rb @@ -0,0 +1,35 @@ +# typed: strict +# frozen_string_literal: true + +require "linux_runner_spec" +require "macos_runner_spec" + +class GitHubRunner < T::Struct + extend T::Sig + + const :platform, Symbol + const :arch, Symbol + const :spec, T.any(LinuxRunnerSpec, MacOSRunnerSpec) + const :macos_version, T.nilable(OS::Mac::Version) + prop :active, T::Boolean, default: false + + sig { returns(T::Boolean) } + def macos? + platform == :macos + end + + sig { returns(T::Boolean) } + def linux? + platform == :linux + end + + sig { returns(T::Boolean) } + def x86_64? + arch == :x86_64 + end + + sig { returns(T::Boolean) } + def arm64? + arch == :arm64 + end +end diff --git a/Library/Homebrew/github_runner_matrix.rb b/Library/Homebrew/github_runner_matrix.rb new file mode 100644 index 0000000000..e2ce5671f2 --- /dev/null +++ b/Library/Homebrew/github_runner_matrix.rb @@ -0,0 +1,188 @@ +# typed: strict +# frozen_string_literal: true + +require "test_runner_formula" +require "github_runner" + +class GitHubRunnerMatrix + extend T::Sig + + # FIXME: Enable cop again when https://github.com/sorbet/sorbet/issues/3532 is fixed. + # rubocop:disable Style/MutableConstant + MaybeStringArray = T.type_alias { T.nilable(T::Array[String]) } + private_constant :MaybeStringArray + + RunnerSpec = T.type_alias { T.any(LinuxRunnerSpec, MacOSRunnerSpec) } + private_constant :RunnerSpec + + MacOSRunnerSpecHash = T.type_alias { { name: String, runner: String, cleanup: T::Boolean } } + private_constant :MacOSRunnerSpecHash + + LinuxRunnerSpecHash = T.type_alias do + { + name: String, + runner: String, + container: T::Hash[Symbol, String], + workdir: String, + timeout: Integer, + cleanup: T::Boolean, + } + end + private_constant :LinuxRunnerSpecHash + + RunnerSpecHash = T.type_alias { T.any(LinuxRunnerSpecHash, MacOSRunnerSpecHash) } + private_constant :RunnerSpecHash + # rubocop:enable Style/MutableConstant + + sig { returns(T::Array[GitHubRunner]) } + attr_reader :runners + + sig { + params( + testing_formulae: T::Array[TestRunnerFormula], + deleted_formulae: MaybeStringArray, + dependent_matrix: T::Boolean, + ).void + } + def initialize(testing_formulae, deleted_formulae, dependent_matrix:) + @testing_formulae = T.let(testing_formulae, T::Array[TestRunnerFormula]) + @deleted_formulae = T.let(deleted_formulae, MaybeStringArray) + @dependent_matrix = T.let(dependent_matrix, T::Boolean) + + @runners = T.let([], T::Array[GitHubRunner]) + generate_runners! + + freeze + end + + sig { returns(T::Array[RunnerSpecHash]) } + def active_runner_specs_hash + runners.select(&:active) + .map(&:spec) + .map(&:to_h) + end + + private + + sig { returns(LinuxRunnerSpec) } + def linux_runner_spec + linux_runner = ENV.fetch("HOMEBREW_LINUX_RUNNER") + linux_cleanup = ENV.fetch("HOMEBREW_LINUX_CLEANUP") + + LinuxRunnerSpec.new( + name: "Linux", + 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", + ) + end + + VALID_PLATFORMS = T.let([:macos, :linux].freeze, T::Array[Symbol]) + VALID_ARCHES = T.let([:arm64, :x86_64].freeze, T::Array[Symbol]) + + sig { + params( + platform: Symbol, + arch: Symbol, + spec: RunnerSpec, + macos_version: T.nilable(OS::Mac::Version), + ).returns(GitHubRunner) + } + def create_runner(platform, arch, spec, macos_version = nil) + raise "Unexpected platform: #{platform}" if VALID_PLATFORMS.exclude?(platform) + raise "Unexpected arch: #{arch}" if VALID_ARCHES.exclude?(arch) + + runner = GitHubRunner.new(platform: platform, arch: arch, spec: spec, macos_version: macos_version) + runner.active = active_runner?(runner) + runner.freeze + end + + sig { void } + def generate_runners! + return if @runners.present? + + @runners << create_runner(:linux, :x86_64, linux_runner_spec) + + github_run_id = ENV.fetch("GITHUB_RUN_ID") + github_run_attempt = ENV.fetch("GITHUB_RUN_ATTEMPT") + ephemeral_suffix = "-#{github_run_id}-#{github_run_attempt}" + + MacOSVersions::SYMBOLS.each_value do |version| + macos_version = OS::Mac::Version.new(version) + next if macos_version.unsupported_release? + + spec = MacOSRunnerSpec.new( + name: "macOS #{version}-x86_64", + runner: "#{version}#{ephemeral_suffix}", + cleanup: false, + ) + @runners << create_runner(:macos, :x86_64, spec, macos_version) + + next unless macos_version >= :big_sur + + # Use bare metal runner when testing dependents on ARM64 Monterey. + runner, cleanup = if (macos_version >= :ventura && @dependent_matrix) || macos_version >= :monterey + ["#{version}-arm64#{ephemeral_suffix}", false] + else + ["#{version}-arm64", true] + end + + spec = MacOSRunnerSpec.new(name: "macOS #{version}-arm64", runner: runner, cleanup: cleanup) + @runners << create_runner(:macos, :arm64, spec, macos_version) + end + + @runners.freeze + end + + sig { params(runner: GitHubRunner).returns(T::Boolean) } + def active_runner?(runner) + if @dependent_matrix + formulae_have_untested_dependents?(runner) + else + return true if @deleted_formulae.present? + + compatible_formulae = @testing_formulae.dup + + platform = runner.platform + arch = runner.arch + macos_version = runner.macos_version + + compatible_formulae.select! { |formula| formula.public_send(:"#{platform}_compatible?") } + compatible_formulae.select! { |formula| formula.public_send(:"#{arch}_compatible?") } + compatible_formulae.select! { |formula| formula.compatible_with?(macos_version) } if macos_version + + compatible_formulae.present? + end + end + + sig { params(runner: GitHubRunner).returns(T::Boolean) } + def formulae_have_untested_dependents?(runner) + platform = runner.platform + arch = runner.arch + macos_version = runner.macos_version + + @testing_formulae.any? do |formula| + # If the formula has a platform/arch/macOS version requirement, then its + # dependents don't need to be tested if these requirements are not satisfied. + next false unless formula.public_send(:"#{platform}_compatible?") + next false unless formula.public_send(:"#{arch}_compatible?") + next false if macos_version.present? && !formula.compatible_with?(macos_version) + + compatible_dependents = formula.dependents(platform: platform, arch: arch, macos_version: macos_version&.to_sym) + .dup + + compatible_dependents.select! { |dependent_f| dependent_f.public_send(:"#{platform}_compatible?") } + compatible_dependents.select! { |dependent_f| dependent_f.public_send(:"#{arch}_compatible?") } + compatible_dependents.select! { |dependent_f| dependent_f.compatible_with?(macos_version) } if macos_version + + # These arrays will generally have been generated by different Formulary caches, + # so we can only compare them by name and not directly. + (compatible_dependents.map(&:name) - @testing_formulae.map(&:name)).present? + end + end +end diff --git a/Library/Homebrew/linux_runner_spec.rb b/Library/Homebrew/linux_runner_spec.rb new file mode 100644 index 0000000000..533eb3034b --- /dev/null +++ b/Library/Homebrew/linux_runner_spec.rb @@ -0,0 +1,34 @@ +# typed: strict +# frozen_string_literal: true + +class LinuxRunnerSpec < T::Struct + extend T::Sig + + const :name, String + const :runner, String + const :container, T::Hash[Symbol, String] + const :workdir, String + const :timeout, Integer + const :cleanup, T::Boolean + + sig { + returns({ + name: String, + runner: String, + container: T::Hash[Symbol, String], + workdir: String, + timeout: Integer, + cleanup: T::Boolean, + }) + } + def to_h + { + name: name, + runner: runner, + container: container, + workdir: workdir, + timeout: timeout, + cleanup: cleanup, + } + end +end diff --git a/Library/Homebrew/macos_runner_spec.rb b/Library/Homebrew/macos_runner_spec.rb new file mode 100644 index 0000000000..eb3b5ff6c4 --- /dev/null +++ b/Library/Homebrew/macos_runner_spec.rb @@ -0,0 +1,19 @@ +# typed: strict +# frozen_string_literal: true + +class MacOSRunnerSpec < T::Struct + extend T::Sig + + const :name, String + const :runner, String + const :cleanup, T::Boolean + + sig { returns({ name: String, runner: String, cleanup: T::Boolean }) } + def to_h + { + name: name, + runner: runner, + cleanup: cleanup, + } + end +end diff --git a/Library/Homebrew/os/mac/version.rb b/Library/Homebrew/os/mac/version.rb index d407e882ac..ce8ebb6880 100644 --- a/Library/Homebrew/os/mac/version.rb +++ b/Library/Homebrew/os/mac/version.rb @@ -72,6 +72,11 @@ module OS self >= HOMEBREW_MACOS_NEWEST_UNSUPPORTED end + sig { returns(T::Boolean) } + def unsupported_release? + outdated_release? || prerelease? + end + # For {OS::Mac::Version} compatibility. sig { returns(T::Boolean) } def requires_nehalem_cpu? diff --git a/Library/Homebrew/requirements/macos_requirement.rb b/Library/Homebrew/requirements/macos_requirement.rb index 6309f5d47a..b6047e1ea1 100644 --- a/Library/Homebrew/requirements/macos_requirement.rb +++ b/Library/Homebrew/requirements/macos_requirement.rb @@ -23,9 +23,9 @@ class MacOSRequirement < Requirement def initialize(tags = [], comparator: ">=") @version = begin if comparator == "==" && tags.first.respond_to?(:map) - tags.first.map { |s| MacOS::Version.from_symbol(s) } + tags.first.map { |s| OS::Mac::Version.from_symbol(s) } else - MacOS::Version.from_symbol(tags.first) unless tags.empty? + OS::Mac::Version.from_symbol(tags.first) unless tags.empty? end rescue MacOSVersionError => e if DISABLED_MACOS_VERSIONS.include?(e.version) @@ -43,7 +43,7 @@ class MacOSRequirement < Requirement end # Otherwise fallback to the oldest allowed if comparator is >=. - MacOS::Version.new(HOMEBREW_MACOS_OLDEST_ALLOWED) if comparator == ">=" + OS::Mac::Version.new(HOMEBREW_MACOS_OLDEST_ALLOWED) if comparator == ">=" end @comparator = comparator @@ -56,7 +56,7 @@ class MacOSRequirement < Requirement satisfy(build_env: false) do T.bind(self, MacOSRequirement) - next Array(@version).any? { |v| MacOS.version.compare(@comparator, v) } if OS.mac? && version_specified? + next Array(@version).any? { |v| OS::Mac.version.compare(@comparator, v) } if OS.mac? && version_specified? next true if OS.mac? next true if @version diff --git a/Library/Homebrew/test/dev-cmd/determine-test-runners_spec.rb b/Library/Homebrew/test/dev-cmd/determine-test-runners_spec.rb new file mode 100644 index 0000000000..769a66a65b --- /dev/null +++ b/Library/Homebrew/test/dev-cmd/determine-test-runners_spec.rb @@ -0,0 +1,75 @@ +# typed: false +# frozen_string_literal: true + +require "dev-cmd/determine-test-runners" +require "cmd/shared_examples/args_parse" + +describe "brew determine-test-runners" do + after do + FileUtils.rm_f github_output + end + + let(:linux_runner) { "ubuntu-22.04" } + # We need to make sure we write to a different path for each example. + let(:github_output) { "#{TEST_TMPDIR}/github_output#{DetermineRunnerTestHelper.new.number}" } + let(:ephemeral_suffix) { "-12345-1" } + let(:runner_env) do + { + "HOMEBREW_LINUX_RUNNER" => linux_runner, + "HOMEBREW_LINUX_CLEANUP" => "false", + "GITHUB_RUN_ID" => ephemeral_suffix.split("-").second, + "GITHUB_RUN_ATTEMPT" => ephemeral_suffix.split("-").third, + }.freeze + end + let(:all_runners) do + out = [] + MacOSVersions::SYMBOLS.each_value do |v| + macos_version = OS::Mac::Version.new(v) + next if macos_version.unsupported_release? + + out << v + out << "#{v}-arm64" + end + + out << linux_runner + + out + end + + it_behaves_like "parseable arguments" + + 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 }) } + .to not_to_output.to_stdout + .and not_to_output.to_stderr + .and be_a_success + + expect(File.read(github_output)).not_to be_empty + expect(get_runners(github_output).sort).to eq(all_runners.sort) + end +end + +def get_runners(file) + runner_line = File.open(file).first + json_text = runner_line[/runners=(.*)/, 1] + runner_hash = JSON.parse(json_text) + runner_hash.map { |item| item["runner"].delete_suffix(ephemeral_suffix) } + .sort +end + +class DetermineRunnerTestHelper + @instances = 0 + + class << self + attr_accessor :instances + end + + attr_reader :number + + def initialize + self.class.instances += 1 + @number = self.class.instances + end +end diff --git a/Library/Homebrew/test/github_runner_matrix_spec.rb b/Library/Homebrew/test/github_runner_matrix_spec.rb new file mode 100644 index 0000000000..37e8260f9d --- /dev/null +++ b/Library/Homebrew/test/github_runner_matrix_spec.rb @@ -0,0 +1,288 @@ +# typed: false +# frozen_string_literal: true + +require "github_runner_matrix" +require "test/support/fixtures/testball" + +describe GitHubRunnerMatrix do + before do + allow(ENV).to receive(:fetch).with("HOMEBREW_LINUX_RUNNER").and_return("ubuntu-latest") + allow(ENV).to receive(:fetch).with("HOMEBREW_LINUX_CLEANUP").and_return("false") + allow(ENV).to receive(:fetch).with("GITHUB_RUN_ID").and_return("12345") + allow(ENV).to receive(:fetch).with("GITHUB_RUN_ATTEMPT").and_return("1") + end + + let(:newest_supported_macos) do + MacOSVersions::SYMBOLS.find { |_, v| !OS::Mac::Version.new(v).prerelease? } + end + + let(:testball) { TestRunnerFormula.new(Testball.new) } + let(:testball_depender) { setup_test_runner_formula("testball-depender", ["testball"]) } + let(:testball_depender_linux) { setup_test_runner_formula("testball-depender-linux", ["testball", :linux]) } + let(:testball_depender_macos) { setup_test_runner_formula("testball-depender-macos", ["testball", :macos]) } + let(:testball_depender_intel) do + setup_test_runner_formula("testball-depender-intel", ["testball", { arch: :x86_64 }]) + end + let(:testball_depender_arm) { setup_test_runner_formula("testball-depender-arm", ["testball", { arch: :arm64 }]) } + let(:testball_depender_newest) do + symbol, = newest_supported_macos + setup_test_runner_formula("testball-depender-newest", ["testball", { macos: symbol }]) + end + + describe "#active_runner_specs_hash" do + it "returns an object that responds to `#to_json`" do + expect( + described_class.new([], ["deleted"], dependent_matrix: false) + .active_runner_specs_hash + .respond_to?(:to_json), + ).to be(true) + end + end + + describe "#generate_runners!" do + it "is idempotent" do + matrix = described_class.new([], [], dependent_matrix: false) + runners = matrix.runners.dup + matrix.send(:generate_runners!) + + expect(matrix.runners).to eq(runners) + end + end + + context "when there are no testing formulae and no deleted formulae" do + it "activates no test runners" do + expect(described_class.new([], [], dependent_matrix: false).runners.any?(&:active)) + .to be(false) + end + + it "activates no dependent runners" do + expect(described_class.new([], [], dependent_matrix: true).runners.any?(&:active)) + .to be(false) + end + end + + context "when there are testing formulae and no deleted formulae" do + context "when it is a matrix for the `tests` job" do + context "when testing formulae have no requirements" do + it "activates all runners" do + expect(described_class.new([testball], [], dependent_matrix: false).runners.all?(&:active)) + .to be(true) + end + end + + context "when testing formulae require Linux" do + it "activates only the Linux runner" do + runner_matrix = described_class.new([testball_depender_linux], [], dependent_matrix: false) + + expect(runner_matrix.runners.all?(&:active)).to be(false) + expect(runner_matrix.runners.any?(&:active)).to be(true) + expect(get_runner_names(runner_matrix)).to eq(["Linux"]) + end + end + + context "when testing formulae require macOS" do + it "activates only the macOS runners" do + runner_matrix = described_class.new([testball_depender_macos], [], dependent_matrix: false) + + expect(runner_matrix.runners.all?(&:active)).to be(false) + expect(runner_matrix.runners.any?(&:active)).to be(true) + expect(get_runner_names(runner_matrix)).to eq(get_runner_names(runner_matrix, :macos?)) + end + end + + context "when testing formulae require Intel" do + it "activates only the Intel runners" do + runner_matrix = described_class.new([testball_depender_intel], [], dependent_matrix: false) + + expect(runner_matrix.runners.all?(&:active)).to be(false) + expect(runner_matrix.runners.any?(&:active)).to be(true) + expect(get_runner_names(runner_matrix)).to eq(get_runner_names(runner_matrix, :x86_64?)) + end + end + + context "when testing formulae require ARM" do + it "activates only the ARM runners" do + runner_matrix = described_class.new([testball_depender_arm], [], dependent_matrix: false) + + expect(runner_matrix.runners.all?(&:active)).to be(false) + expect(runner_matrix.runners.any?(&:active)).to be(true) + expect(get_runner_names(runner_matrix)).to eq(get_runner_names(runner_matrix, :arm64?)) + end + end + + context "when testing formulae require a macOS version" do + it "activates the Linux runner and suitable macOS runners" do + _, v = newest_supported_macos + runner_matrix = described_class.new([testball_depender_newest], [], dependent_matrix: false) + + expect(runner_matrix.runners.all?(&:active)).to be(false) + expect(runner_matrix.runners.any?(&:active)).to be(true) + expect(get_runner_names(runner_matrix).sort).to eq(["Linux", "macOS #{v}-arm64", "macOS #{v}-x86_64"]) + end + end + end + + context "when it is a matrix for the `test_deps` job" do + context "when testing formulae have no dependents" do + it "activates no runners" do + allow(Homebrew::EnvConfig).to receive(:eval_all?).and_return(true) + allow(Formula).to receive(:all).and_return([testball].map(&:formula)) + + expect(described_class.new([testball], [], dependent_matrix: true).runners.any?(&:active)) + .to be(false) + end + end + + context "when testing formulae have dependents" do + context "when dependents have no requirements" do + it "activates all runners" do + allow(Homebrew::EnvConfig).to receive(:eval_all?).and_return(true) + allow(Formula).to receive(:all).and_return([testball, testball_depender].map(&:formula)) + + expect(described_class.new([testball], [], dependent_matrix: true).runners.all?(&:active)) + .to be(true) + end + end + + context "when dependents require Linux" do + it "activates only Linux runners" do + allow(Homebrew::EnvConfig).to receive(:eval_all?).and_return(true) + allow(Formula).to receive(:all).and_return([testball, testball_depender_linux].map(&:formula)) + + runner_matrix = described_class.new([testball], [], dependent_matrix: true) + expect(runner_matrix.runners.all?(&:active)).to be(false) + expect(runner_matrix.runners.any?(&:active)).to be(true) + expect(get_runner_names(runner_matrix)).to eq(get_runner_names(runner_matrix, :linux?)) + end + end + + context "when dependents require macOS" do + it "activates only macOS runners" do + allow(Homebrew::EnvConfig).to receive(:eval_all?).and_return(true) + allow(Formula).to receive(:all).and_return([testball, testball_depender_macos].map(&:formula)) + + runner_matrix = described_class.new([testball], [], dependent_matrix: true) + expect(runner_matrix.runners.all?(&:active)).to be(false) + expect(runner_matrix.runners.any?(&:active)).to be(true) + expect(get_runner_names(runner_matrix)).to eq(get_runner_names(runner_matrix, :macos?)) + end + end + + context "when dependents require an Intel architecture" do + it "activates only Intel runners" do + allow(Homebrew::EnvConfig).to receive(:eval_all?).and_return(true) + allow(Formula).to receive(:all).and_return([testball, testball_depender_intel].map(&:formula)) + + runner_matrix = described_class.new([testball], [], dependent_matrix: true) + expect(runner_matrix.runners.all?(&:active)).to be(false) + expect(runner_matrix.runners.any?(&:active)).to be(true) + expect(get_runner_names(runner_matrix)).to eq(get_runner_names(runner_matrix, :x86_64?)) + end + end + + context "when dependents require an ARM architecture" do + it "activates only ARM runners" do + allow(Homebrew::EnvConfig).to receive(:eval_all?).and_return(true) + allow(Formula).to receive(:all).and_return([testball, testball_depender_arm].map(&:formula)) + + runner_matrix = described_class.new([testball], [], dependent_matrix: true) + expect(runner_matrix.runners.all?(&:active)).to be(false) + expect(runner_matrix.runners.any?(&:active)).to be(true) + expect(get_runner_names(runner_matrix)).to eq(get_runner_names(runner_matrix, :arm64?)) + end + end + end + end + end + + context "when there are deleted formulae" do + context "when it is a matrix for the `tests` job" do + it "activates all runners" do + expect(described_class.new([], ["deleted"], dependent_matrix: false).runners.all?(&:active)) + .to be(true) + end + end + + context "when it is a matrix for the `test_deps` job" do + context "when there are no testing formulae" do + it "activates no runners" do + expect(described_class.new([], ["deleted"], dependent_matrix: true).runners.any?(&:active)) + .to be(false) + end + end + + context "when there are testing formulae with no dependents" do + it "activates no runners" do + testing_formulae = [testball] + runner_matrix = described_class.new(testing_formulae, ["deleted"], dependent_matrix: true) + + allow(Homebrew::EnvConfig).to receive(:eval_all?).and_return(true) + allow(Formula).to receive(:all).and_return(testing_formulae.map(&:formula)) + + expect(runner_matrix.runners.none?(&:active)).to be(true) + end + end + + context "when there are testing formulae with dependents" do + context "when dependent formulae have no requirements" do + it "activates the applicable runners" do + allow(Homebrew::EnvConfig).to receive(:eval_all?).and_return(true) + allow(Formula).to receive(:all).and_return([testball, testball_depender].map(&:formula)) + + testing_formulae = [testball] + expect(described_class.new(testing_formulae, ["deleted"], dependent_matrix: true).runners.all?(&:active)) + .to be(true) + end + end + + context "when dependent formulae have requirements" do + context "when dependent formulae require Linux" do + it "activates the applicable runners" do + allow(Homebrew::EnvConfig).to receive(:eval_all?).and_return(true) + allow(Formula).to receive(:all).and_return([testball, testball_depender_linux].map(&:formula)) + + testing_formulae = [testball] + matrix = described_class.new(testing_formulae, ["deleted"], dependent_matrix: true) + expect(get_runner_names(matrix)).to eq(["Linux"]) + end + end + + context "when dependent formulae require macOS" do + it "activates the applicable runners" do + allow(Homebrew::EnvConfig).to receive(:eval_all?).and_return(true) + allow(Formula).to receive(:all).and_return([testball, testball_depender_macos].map(&:formula)) + + testing_formulae = [testball] + matrix = described_class.new(testing_formulae, ["deleted"], dependent_matrix: true) + expect(get_runner_names(matrix)).to eq(get_runner_names(matrix, :macos?)) + end + end + end + end + end + end + + def get_runner_names(runner_matrix, predicate = :active) + runner_matrix.runners + .select(&predicate) + .map(&:spec) + .map(&:name) + end + + def setup_test_runner_formula(name, dependencies = [], **kwargs) + f = formula name do + url "https://brew.sh/#{name}-1.0.tar.gz" + dependencies.each { |dependency| depends_on dependency } + + kwargs.each do |k, v| + send(:"on_#{k}") do + v.each do |dep| + depends_on dep + end + end + end + end + + TestRunnerFormula.new(f) + end +end diff --git a/Library/Homebrew/test/github_runner_spec.rb b/Library/Homebrew/test/github_runner_spec.rb new file mode 100644 index 0000000000..04cfa8d595 --- /dev/null +++ b/Library/Homebrew/test/github_runner_spec.rb @@ -0,0 +1,34 @@ +# typed: false +# frozen_string_literal: true + +require "github_runner" + +describe GitHubRunner do + let(:runner) do + spec = MacOSRunnerSpec.new(name: "macOS 11-arm64", runner: "11-arm64", cleanup: true) + version = OS::Mac::Version.new("11") + described_class.new(platform: :macos, arch: :arm64, spec: spec, macos_version: version) + end + + it "has immutable attributes" do + [:platform, :arch, :spec, :macos_version].each do |attribute| + expect(runner.respond_to?("#{attribute}=")).to be(false) + end + end + + it "is inactive by default" do + expect(runner.active).to be(false) + end + + describe "#macos?" do + it "returns true if the runner is a macOS runner" do + expect(runner.macos?).to be(true) + end + end + + describe "#linux?" do + it "returns false if the runner is a macOS runner" do + expect(runner.linux?).to be(false) + end + end +end diff --git a/Library/Homebrew/test/linux_runner_spec_spec.rb b/Library/Homebrew/test/linux_runner_spec_spec.rb new file mode 100644 index 0000000000..0f3add6594 --- /dev/null +++ b/Library/Homebrew/test/linux_runner_spec_spec.rb @@ -0,0 +1,29 @@ +# typed: false +# frozen_string_literal: true + +require "linux_runner_spec" + +describe LinuxRunnerSpec do + let(:spec) do + described_class.new( + name: "Linux", + runner: "ubuntu-latest", + container: { image: "ghcr.io/homebrew/ubuntu22.04:master", options: "--user=linuxbrew" }, + workdir: "/github/home", + timeout: 360, + cleanup: false, + ) + end + + it "has immutable attributes" do + [:name, :runner, :container, :workdir, :timeout, :cleanup].each do |attribute| + expect(spec.respond_to?("#{attribute}=")).to be(false) + end + end + + describe "#to_h" do + it "returns an object that responds to `#to_json`" do + expect(spec.to_h.respond_to?(:to_json)).to be(true) + end + end +end diff --git a/Library/Homebrew/test/macos_runner_spec_spec.rb b/Library/Homebrew/test/macos_runner_spec_spec.rb new file mode 100644 index 0000000000..3c56c72805 --- /dev/null +++ b/Library/Homebrew/test/macos_runner_spec_spec.rb @@ -0,0 +1,20 @@ +# typed: false +# frozen_string_literal: true + +require "macos_runner_spec" + +describe MacOSRunnerSpec do + let(:spec) { described_class.new(name: "macOS 11-arm64", runner: "11-arm64", cleanup: true) } + + it "has immutable attributes" do + [:name, :runner, :cleanup].each do |attribute| + expect(spec.respond_to?("#{attribute}=")).to be(false) + end + end + + describe "#to_h" do + it "returns an object that responds to `#to_json`" do + expect(spec.to_h.respond_to?(:to_json)).to be(true) + end + end +end diff --git a/Library/Homebrew/test/test_runner_formula_spec.rb b/Library/Homebrew/test/test_runner_formula_spec.rb new file mode 100644 index 0000000000..54fcd36477 --- /dev/null +++ b/Library/Homebrew/test/test_runner_formula_spec.rb @@ -0,0 +1,467 @@ +# typed: false +# frozen_string_literal: true + +require "test_runner_formula" +require "test/support/fixtures/testball" + +describe TestRunnerFormula do + let(:testball) { Testball.new } + let(:xcode_helper) { setup_test_formula("xcode-helper", [:macos]) } + let(:linux_kernel_requirer) { setup_test_formula("linux-kernel-requirer", [:linux]) } + let(:old_non_portable_software) { setup_test_formula("old-non-portable-software", [arch: :x86_64]) } + let(:fancy_new_software) { setup_test_formula("fancy-new-software", [arch: :arm64]) } + let(:needs_modern_compiler) { setup_test_formula("needs-modern-compiler", [macos: :ventura]) } + + describe "#initialize" do + it "enables the Formulary factory cache" do + described_class.new(testball) + expect(Formulary.factory_cached?).to be(true) + end + end + + describe "#name" do + it "returns the wrapped Formula's name" do + expect(described_class.new(testball).name).to eq(testball.name) + end + end + + describe "#eval_all" do + it "is false by default" do + expect(described_class.new(testball).eval_all).to be(false) + end + + it "can be instantiated to be `true`" do + expect(described_class.new(testball, eval_all: true).eval_all).to be(true) + end + + it "takes the value of `HOMEBREW_EVAL_ALL` at instantiation time if not specified" do + allow(Homebrew::EnvConfig).to receive(:eval_all?).and_return(true) + expect(described_class.new(testball).eval_all).to be(true) + + allow(Homebrew::EnvConfig).to receive(:eval_all?).and_return(false) + expect(described_class.new(testball).eval_all).to be(false) + end + end + + describe "#formula" do + it "returns the wrapped Formula" do + expect(described_class.new(testball).formula).to eq(testball) + end + end + + describe "#macos_only?" do + context "when a formula requires macOS" do + it "returns true" do + expect(described_class.new(xcode_helper).macos_only?).to be(true) + end + end + + context "when a formula does not require macOS" do + it "returns false" do + expect(described_class.new(testball).macos_only?).to be(false) + expect(described_class.new(linux_kernel_requirer).macos_only?).to be(false) + expect(described_class.new(old_non_portable_software).macos_only?).to be(false) + expect(described_class.new(fancy_new_software).macos_only?).to be(false) + end + end + + context "when a formula requires only a minimum version of macOS" do + it "returns false" do + expect(described_class.new(needs_modern_compiler).macos_only?).to be(false) + end + end + end + + describe "#macos_compatible?" do + context "when a formula is compatible with macOS" do + it "returns true" do + expect(described_class.new(testball).macos_compatible?).to be(true) + expect(described_class.new(xcode_helper).macos_compatible?).to be(true) + expect(described_class.new(old_non_portable_software).macos_compatible?).to be(true) + expect(described_class.new(fancy_new_software).macos_compatible?).to be(true) + end + end + + context "when a formula requires only a minimum version of macOS" do + it "returns false" do + expect(described_class.new(needs_modern_compiler).macos_compatible?).to be(true) + end + end + + context "when a formula is not compatible with macOS" do + it "returns false" do + expect(described_class.new(linux_kernel_requirer).macos_compatible?).to be(false) + end + end + end + + describe "#linux_only?" do + context "when a formula requires Linux" do + it "returns true" do + expect(described_class.new(linux_kernel_requirer).linux_only?).to be(true) + end + end + + context "when a formula does not require Linux" do + it "returns false" do + expect(described_class.new(testball).linux_only?).to be(false) + expect(described_class.new(xcode_helper).linux_only?).to be(false) + expect(described_class.new(old_non_portable_software).linux_only?).to be(false) + expect(described_class.new(fancy_new_software).linux_only?).to be(false) + expect(described_class.new(needs_modern_compiler).linux_only?).to be(false) + end + end + end + + describe "#linux_compatible?" do + context "when a formula is compatible with Linux" do + it "returns true" do + expect(described_class.new(testball).linux_compatible?).to be(true) + expect(described_class.new(linux_kernel_requirer).linux_compatible?).to be(true) + expect(described_class.new(old_non_portable_software).linux_compatible?).to be(true) + expect(described_class.new(fancy_new_software).linux_compatible?).to be(true) + expect(described_class.new(needs_modern_compiler).linux_compatible?).to be(true) + end + end + + context "when a formula is not compatible with Linux" do + it "returns false" do + expect(described_class.new(xcode_helper).linux_compatible?).to be(false) + end + end + end + + describe "#x86_64_only?" do + context "when a formula requires an Intel architecture" do + it "returns true" do + expect(described_class.new(old_non_portable_software).x86_64_only?).to be(true) + end + end + + context "when a formula requires a non-Intel architecture" do + it "returns false" do + expect(described_class.new(fancy_new_software).x86_64_only?).to be(false) + end + end + + context "when a formula does not require a specfic architecture" do + it "returns false" do + expect(described_class.new(testball).x86_64_only?).to be(false) + expect(described_class.new(xcode_helper).x86_64_only?).to be(false) + expect(described_class.new(linux_kernel_requirer).x86_64_only?).to be(false) + expect(described_class.new(needs_modern_compiler).x86_64_only?).to be(false) + end + end + end + + describe "#x86_64_compatible?" do + context "when a formula is compatible with the Intel architecture" do + it "returns true" do + expect(described_class.new(testball).x86_64_compatible?).to be(true) + expect(described_class.new(xcode_helper).x86_64_compatible?).to be(true) + expect(described_class.new(linux_kernel_requirer).x86_64_compatible?).to be(true) + expect(described_class.new(old_non_portable_software).x86_64_compatible?).to be(true) + expect(described_class.new(needs_modern_compiler).x86_64_compatible?).to be(true) + end + end + + context "when a formula is not compatible with the Intel architecture" do + it "returns false" do + expect(described_class.new(fancy_new_software).x86_64_compatible?).to be(false) + end + end + end + + describe "#arm64_only?" do + context "when a formula requires an ARM64 architecture" do + it "returns true" do + expect(described_class.new(fancy_new_software).arm64_only?).to be(true) + end + end + + context "when a formula requires a non-ARM64 architecture" do + it "returns false" do + expect(described_class.new(old_non_portable_software).arm64_only?).to be(false) + end + end + + context "when a formula does not require a specific architecture" do + it "returns false" do + expect(described_class.new(testball).arm64_only?).to be(false) + expect(described_class.new(xcode_helper).arm64_only?).to be(false) + expect(described_class.new(linux_kernel_requirer).arm64_only?).to be(false) + expect(described_class.new(needs_modern_compiler).arm64_only?).to be(false) + end + end + end + + describe "#arm64_compatible?" do + context "when a formula is compatible with an ARM64 architecture" do + it "returns true" do + expect(described_class.new(testball).arm64_compatible?).to be(true) + expect(described_class.new(xcode_helper).arm64_compatible?).to be(true) + expect(described_class.new(linux_kernel_requirer).arm64_compatible?).to be(true) + expect(described_class.new(fancy_new_software).arm64_compatible?).to be(true) + expect(described_class.new(needs_modern_compiler).arm64_compatible?).to be(true) + end + end + + context "when a formula is not compatible with an ARM64 architecture" do + it "returns false" do + expect(described_class.new(old_non_portable_software).arm64_compatible?).to be(false) + end + end + end + + describe "#versioned_macos_requirement" do + let(:requirement) { described_class.new(needs_modern_compiler).versioned_macos_requirement } + + it "returns a MacOSRequirement with a specified version" do + expect(requirement).to be_a(MacOSRequirement) + expect(requirement.version_specified?).to be(true) + end + + context "when a formula has an unversioned MacOSRequirement" do + it "returns nil" do + expect(described_class.new(xcode_helper).versioned_macos_requirement).to be_nil + end + end + + context "when a formula has no declared MacOSRequirement" do + it "returns nil" do + expect(described_class.new(testball).versioned_macos_requirement).to be_nil + expect(described_class.new(linux_kernel_requirer).versioned_macos_requirement).to be_nil + expect(described_class.new(old_non_portable_software).versioned_macos_requirement).to be_nil + expect(described_class.new(fancy_new_software).versioned_macos_requirement).to be_nil + end + end + end + + describe "#compatible_with?" do + context "when a formula has a versioned MacOSRequirement" do + context "when passed a compatible macOS version" do + it "returns true" do + expect(described_class.new(needs_modern_compiler).compatible_with?(OS::Mac::Version.new("13"))) + .to be(true) + end + end + + context "when passed an incompatible macOS version" do + it "returns false" do + expect(described_class.new(needs_modern_compiler).compatible_with?(OS::Mac::Version.new("11"))) + .to be(false) + end + end + end + + context "when a formula has an unversioned MacOSRequirement" do + it "returns true" do + MacOSVersions::SYMBOLS.each_value do |v| + version = OS::Mac::Version.new(v) + expect(described_class.new(xcode_helper).compatible_with?(version)).to be(true) + end + end + end + + context "when a formula has no declared MacOSRequirement" do + it "returns true" do + MacOSVersions::SYMBOLS.each_value do |v| + version = OS::Mac::Version.new(v) + expect(described_class.new(testball).compatible_with?(version)).to be(true) + expect(described_class.new(linux_kernel_requirer).compatible_with?(version)).to be(true) + expect(described_class.new(old_non_portable_software).compatible_with?(version)).to be(true) + expect(described_class.new(fancy_new_software).compatible_with?(version)).to be(true) + end + end + end + end + + describe "#dependents" do + let(:current_system) do + current_arch = case Homebrew::SimulateSystem.current_arch + when :arm then :arm64 + when :intel then :x86_64 + end + + current_platform = case Homebrew::SimulateSystem.current_os + when :generic then :linux + else Homebrew::SimulateSystem.current_os + end + + { + platform: current_platform, + arch: current_arch, + macos_version: nil, + } + end + + context "when a formula has no dependents" do + it "returns an empty array" do + expect(described_class.new(testball).dependents(current_system)).to eq([]) + expect(described_class.new(xcode_helper).dependents(current_system)).to eq([]) + expect(described_class.new(linux_kernel_requirer).dependents(current_system)).to eq([]) + expect(described_class.new(old_non_portable_software).dependents(current_system)).to eq([]) + expect(described_class.new(fancy_new_software).dependents(current_system)).to eq([]) + expect(described_class.new(needs_modern_compiler).dependents(current_system)).to eq([]) + + expect(Homebrew::SimulateSystem.os).to be_nil + expect(Homebrew::SimulateSystem.arch).to be_nil + end + end + + context "when a formula has dependents" do + let(:testball_user) { setup_test_formula("testball_user", ["testball"]) } + let(:recursive_testball_dependent) { setup_test_formula("recursive_testball_dependent", ["testball_user"]) } + + it "returns an array of direct dependents" do + allow(Formula).to receive(:all).and_return([testball_user, recursive_testball_dependent]) + + expect( + described_class.new(testball, eval_all: true).dependents(current_system).map(&:name), + ).to eq(["testball_user"]) + + expect( + described_class.new(testball_user, eval_all: true).dependents(current_system).map(&:name), + ).to eq(["recursive_testball_dependent"]) + + expect(Homebrew::SimulateSystem.os).to be_nil + expect(Homebrew::SimulateSystem.arch).to be_nil + end + + context "when called with arguments" do + let(:testball_user_intel) { setup_test_formula("testball_user-intel", intel: ["testball"]) } + let(:testball_user_arm) { setup_test_formula("testball_user-arm", arm: ["testball"]) } + let(:testball_user_macos) { setup_test_formula("testball_user-macos", macos: ["testball"]) } + let(:testball_user_linux) { setup_test_formula("testball_user-linux", linux: ["testball"]) } + let(:testball_user_ventura) do + setup_test_formula("testball_user-ventura", ventura: ["testball"]) + end + let(:testball_and_dependents) do + [ + testball_user, + testball_user_intel, + testball_user_arm, + testball_user_macos, + testball_user_linux, + testball_user_ventura, + ] + end + + context "when given { platform: :linux, arch: :x86_64 }" do + before do + Homebrew::SimulateSystem.os = :linux + Homebrew::SimulateSystem.arch = :intel + end + + it "returns only the dependents for the requested platform and architecture" do + allow(Formula).to receive(:all).and_return(testball_and_dependents) + + expect( + described_class.new(testball, eval_all: true).dependents( + platform: :linux, arch: :x86_64, macos_version: nil, + ).map(&:name).sort, + ).to eq(["testball_user", "testball_user-intel", "testball_user-linux"].sort) + + expect(Homebrew::SimulateSystem.os).to be_nil + expect(Homebrew::SimulateSystem.arch).to be_nil + end + end + + context "when given { platform: :macos, arch: :x86_64 }" do + before do + Homebrew::SimulateSystem.os = :macos + Homebrew::SimulateSystem.arch = :intel + end + + it "returns only the dependents for the requested platform and architecture" do + allow(Formula).to receive(:all).and_return(testball_and_dependents) + + expect( + described_class.new(testball, eval_all: true).dependents( + platform: :macos, arch: :x86_64, macos_version: nil, + ).map(&:name).sort, + ).to eq(["testball_user", "testball_user-intel", "testball_user-macos"].sort) + + expect(Homebrew::SimulateSystem.os).to be_nil + expect(Homebrew::SimulateSystem.arch).to be_nil + end + end + + context "when given `{ platform: :macos, arch: :arm64 }`" do + before do + Homebrew::SimulateSystem.os = :macos + Homebrew::SimulateSystem.arch = :arm + end + + it "returns only the dependents for the requested platform and architecture" do + allow(Formula).to receive(:all).and_return(testball_and_dependents) + + expect( + described_class.new(testball, eval_all: true).dependents( + platform: :macos, arch: :arm64, macos_version: nil, + ).map(&:name).sort, + ).to eq(["testball_user", "testball_user-arm", "testball_user-macos"].sort) + + expect(Homebrew::SimulateSystem.os).to be_nil + expect(Homebrew::SimulateSystem.arch).to be_nil + end + end + + context "when given `{ platform: :macos, arch: :x86_64, macos_version: :mojave }`" do + before do + Homebrew::SimulateSystem.os = :mojave + Homebrew::SimulateSystem.arch = :intel + end + + it "returns only the dependents for the requested platform and architecture" do + allow(Formula).to receive(:all).and_return(testball_and_dependents) + + expect( + described_class.new(testball, eval_all: true).dependents( + platform: :macos, arch: :x86_64, macos_version: :mojave, + ).map(&:name).sort, + ).to eq(["testball_user", "testball_user-intel", "testball_user-macos"].sort) + + expect(Homebrew::SimulateSystem.os).to be_nil + expect(Homebrew::SimulateSystem.arch).to be_nil + end + end + + context "when given `{ platform: :macos, arch: :arm64, macos_version: :ventura }`" do + before do + Homebrew::SimulateSystem.os = :ventura + Homebrew::SimulateSystem.arch = :arm + end + + it "returns only the dependents for the requested platform and architecture" do + allow(Formula).to receive(:all).and_return(testball_and_dependents) + + expect( + described_class.new(testball, eval_all: true).dependents( + platform: :macos, arch: :arm64, macos_version: :ventura, + ).map(&:name).sort, + ).to eq(%w[testball_user testball_user-arm testball_user-macos testball_user-ventura].sort) + + expect(Homebrew::SimulateSystem.os).to be_nil + expect(Homebrew::SimulateSystem.arch).to be_nil + end + end + end + end + end + + def setup_test_formula(name, dependencies = [], **kwargs) + formula name do + url "https://brew.sh/#{name}-1.0.tar.gz" + dependencies.each { |dependency| depends_on dependency } + + kwargs.each do |k, v| + send(:"on_#{k}") do + v.each do |dep| + depends_on dep + end + end + end + end + end +end diff --git a/Library/Homebrew/test_runner_formula.rb b/Library/Homebrew/test_runner_formula.rb new file mode 100644 index 0000000000..b3e2dded65 --- /dev/null +++ b/Library/Homebrew/test_runner_formula.rb @@ -0,0 +1,116 @@ +# 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 { returns(T::Boolean) } + attr_reader :eval_all + + sig { params(formula: Formula, eval_all: T::Boolean).void } + def initialize(formula, eval_all: Homebrew::EnvConfig.eval_all?) + Formulary.enable_factory_cache! + @formula = T.let(formula, Formula) + @name = T.let(formula.name, String) + @dependent_hash = T.let({}, T::Hash[Symbol, T::Array[TestRunnerFormula]]) + @eval_all = T.let(eval_all, T::Boolean) + 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 macos_compatible? + !linux_only? + end + + sig { returns(T::Boolean) } + def linux_only? + formula.requirements.any?(LinuxRequirement) + end + + sig { returns(T::Boolean) } + def linux_compatible? + !macos_only? + 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 x86_64_compatible? + !arm64_only? + end + + sig { returns(T::Boolean) } + def arm64_only? + formula.requirements.any? { |r| r.is_a?(ArchRequirement) && (r.arch == :arm64) } + end + + sig { returns(T::Boolean) } + def arm64_compatible? + !x86_64_only? + 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: OS::Mac::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 + + SIMULATE_SYSTEM_SYMBOLS = T.let({ arm64: :arm, x86_64: :intel }.freeze, T::Hash[Symbol, Symbol]) + + sig { + params( + platform: Symbol, + arch: Symbol, + macos_version: T.nilable(Symbol), + ).returns(T::Array[TestRunnerFormula]) + } + def dependents(platform:, arch:, macos_version:) + cache_key = :"#{platform}_#{arch}_#{macos_version}" + + @dependent_hash.fetch(cache_key) do + all = eval_all || Homebrew::EnvConfig.eval_all? + formula_selector, eval_all_env = if all + [:all, "1"] + else + [:installed, nil] + end + + with_env(HOMEBREW_EVAL_ALL: eval_all_env) do + Formulary.clear_cache + Homebrew::SimulateSystem.arch = SIMULATE_SYSTEM_SYMBOLS.fetch(arch) + Homebrew::SimulateSystem.os = macos_version || platform + + Formula.public_send(formula_selector) + .select { |candidate_f| candidate_f.deps.map(&:name).include?(name) } + .map { |f| TestRunnerFormula.new(f, eval_all: all) } + .freeze + ensure + Homebrew::SimulateSystem.clear + end + end + end +end diff --git a/completions/bash/brew b/completions/bash/brew index 6020b533b1..893ce249f4 100644 --- a/completions/bash/brew +++ b/completions/bash/brew @@ -787,6 +787,24 @@ _brew_desc() { __brew_complete_casks } +_brew_determine_test_runners() { + local cur="${COMP_WORDS[COMP_CWORD]}" + case "${cur}" in + -*) + __brewcomp " + --debug + --dependents + --eval-all + --help + --quiet + --verbose + " + return + ;; + *) ;; + esac +} + _brew_developer() { local cur="${COMP_WORDS[COMP_CWORD]}" case "${cur}" in @@ -2627,6 +2645,7 @@ _brew() { create) _brew_create ;; deps) _brew_deps ;; desc) _brew_desc ;; + determine-test-runners) _brew_determine_test_runners ;; developer) _brew_developer ;; dispatch-build-bottle) _brew_dispatch_build_bottle ;; docs) _brew_docs ;; diff --git a/completions/fish/brew.fish b/completions/fish/brew.fish index 949764838d..7d28d645f8 100644 --- a/completions/fish/brew.fish +++ b/completions/fish/brew.fish @@ -612,6 +612,15 @@ __fish_brew_complete_arg 'desc; and not __fish_seen_argument -l cask -l casks' - __fish_brew_complete_arg 'desc; and not __fish_seen_argument -l formula -l formulae' -a '(__fish_brew_suggest_casks_all)' +__fish_brew_complete_cmd 'determine-test-runners' 'Determines the runners used to test formulae or their dependents' +__fish_brew_complete_arg 'determine-test-runners' -l debug -d 'Display any debugging information' +__fish_brew_complete_arg 'determine-test-runners' -l dependents -d 'Determine runners for testing dependents. Requires `--eval-all` or `HOMEBREW_EVAL_ALL`' +__fish_brew_complete_arg 'determine-test-runners' -l eval-all -d 'Evaluate all available formulae, whether installed or not, to determine testing dependents' +__fish_brew_complete_arg 'determine-test-runners' -l help -d 'Show this message' +__fish_brew_complete_arg 'determine-test-runners' -l quiet -d 'Make some output more quiet' +__fish_brew_complete_arg 'determine-test-runners' -l verbose -d 'Make some output more verbose' + + __fish_brew_complete_cmd 'developer' 'Control Homebrew\'s developer mode' __fish_brew_complete_sub_cmd 'developer' 'state' __fish_brew_complete_sub_cmd 'developer' 'on' diff --git a/completions/internal_commands_list.txt b/completions/internal_commands_list.txt index ce9b70c9c6..a35e46fe4b 100644 --- a/completions/internal_commands_list.txt +++ b/completions/internal_commands_list.txt @@ -30,6 +30,7 @@ contributions create deps desc +determine-test-runners developer dispatch-build-bottle docs diff --git a/completions/zsh/_brew b/completions/zsh/_brew index bfcfb0dd0a..f97070d420 100644 --- a/completions/zsh/_brew +++ b/completions/zsh/_brew @@ -158,6 +158,7 @@ __brew_internal_commands() { 'create:Generate a formula or, with `--cask`, a cask for the downloadable file at URL and open it in the editor' 'deps:Show dependencies for formula' 'desc:Display formula'\''s name and one-line description' + 'determine-test-runners:Determines the runners used to test formulae or their dependents' 'developer:Control Homebrew'\''s developer mode' 'dispatch-build-bottle:Build bottles for these formulae with GitHub Actions' 'docs:Open Homebrew'\''s online documentation (https://docs.brew.sh) in a browser' @@ -754,6 +755,17 @@ _brew_desc() { '*::cask:__brew_casks' } +# brew determine-test-runners +_brew_determine_test_runners() { + _arguments \ + '--debug[Display any debugging information]' \ + '--dependents[Determine runners for testing dependents. Requires `--eval-all` or `HOMEBREW_EVAL_ALL`]' \ + '--eval-all[Evaluate all available formulae, whether installed or not, to determine testing dependents]' \ + '--help[Show this message]' \ + '--quiet[Make some output more quiet]' \ + '--verbose[Make some output more verbose]' +} + # brew developer _brew_developer() { _arguments \