Merge pull request #15131 from Homebrew/test-runners

dev-cmd/determine-test-runners: add command to set test runners
This commit is contained in:
Carlo Cabrera 2023-04-10 22:42:34 +08:00 committed by GitHub
commit 4e20760a76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1410 additions and 6 deletions

View File

@ -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` <testing-formulae> [<deleted-formulae>]
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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 ;;

View File

@ -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'

View File

@ -30,6 +30,7 @@ contributions
create
deps
desc
determine-test-runners
developer
dispatch-build-bottle
docs

View File

@ -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 \