brew/Library/Homebrew/test/formulary_spec.rb
apainintheneck 226239da4c tests: remove unnecessary cache clearing
This PR removes all remaining unnecessary cache clearing in tests
from the codebase since we now clear all cachable classes between
tests making this functionally unnecessary.

Original PR to automatically clear caches:
- https://github.com/Homebrew/brew/pull/16746

I also moved the `Utils::Analytics` module to use cachable so
that we don't have to clear caches specifically in tests anymore.
2024-03-31 18:38:03 -07:00

648 lines
21 KiB
Ruby

# frozen_string_literal: true
require "formula"
require "formula_installer"
require "utils/bottles"
RSpec.describe Formulary do
let(:formula_name) { "testball_bottle" }
let(:formula_path) { CoreTap.instance.new_formula_path(formula_name) }
let(:formula_content) do
<<~RUBY
class #{described_class.class_s(formula_name)} < Formula
url "file://#{TEST_FIXTURE_DIR}/tarballs/testball-0.1.tbz"
sha256 TESTBALL_SHA256
bottle do
root_url "file://#{bottle_dir}"
sha256 cellar: :any_skip_relocation, #{Utils::Bottles.tag}: "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97"
end
def install
prefix.install "bin"
prefix.install "libexec"
end
end
RUBY
end
let(:bottle_dir) { Pathname.new("#{TEST_FIXTURE_DIR}/bottles") }
let(:bottle) { bottle_dir/"testball_bottle-0.1.#{Utils::Bottles.tag}.bottle.tar.gz" }
describe "::class_s" do
it "replaces '+' with 'x'" do
expect(described_class.class_s("foo++")).to eq("Fooxx")
end
it "converts a string with dots to PascalCase" do
expect(described_class.class_s("shell.fm")).to eq("ShellFm")
end
it "converts a string with hyphens to PascalCase" do
expect(described_class.class_s("pkg-config")).to eq("PkgConfig")
end
it "converts a string with a single letter separated by a hyphen to PascalCase" do
expect(described_class.class_s("s-lang")).to eq("SLang")
end
it "converts a string with underscores to PascalCase" do
expect(described_class.class_s("foo_bar")).to eq("FooBar")
end
it "replaces '@' with 'AT'" do
expect(described_class.class_s("openssl@1.1")).to eq("OpensslAT11")
end
end
describe "::factory" do
before do
formula_path.dirname.mkpath
formula_path.write formula_content
end
it "returns a Formula" do
expect(described_class.factory(formula_name)).to be_a(Formula)
end
it "returns a Formula when given a fully qualified name" do
expect(described_class.factory("homebrew/core/#{formula_name}")).to be_a(Formula)
end
it "raises an error if the Formula cannot be found" do
expect do
described_class.factory("not_existed_formula")
end.to raise_error(FormulaUnavailableError)
end
it "raises an error if ref is nil" do
expect do
described_class.factory(nil)
end.to raise_error(TypeError)
end
context "with sharded Formula directory" do
let(:formula_name) { "testball_sharded" }
let(:formula_path) do
core_tap = CoreTap.instance
(core_tap.formula_dir/formula_name[0]).mkpath
core_tap.new_formula_path(formula_name)
end
it "returns a Formula" do
expect(described_class.factory(formula_name)).to be_a(Formula)
end
it "returns a Formula when given a fully qualified name" do
expect(described_class.factory("homebrew/core/#{formula_name}")).to be_a(Formula)
end
end
context "when the Formula has the wrong class" do
let(:formula_name) { "giraffe" }
let(:formula_content) do
<<~RUBY
class Wrong#{described_class.class_s(formula_name)} < Formula
end
RUBY
end
it "raises an error" do
expect do
described_class.factory(formula_name)
end.to raise_error(TapFormulaClassUnavailableError)
end
end
it "returns a Formula when given a path" do
expect(described_class.factory(formula_path)).to be_a(Formula)
end
it "returns a Formula when given a URL", :needs_utils_curl, :no_api do
formula = described_class.factory("file://#{formula_path}")
expect(formula).to be_a(Formula)
end
context "when given a bottle" do
subject(:formula) { described_class.factory(bottle) }
it "returns a Formula" do
expect(formula).to be_a(Formula)
end
it "calling #local_bottle_path on the returned Formula returns the bottle path" do
expect(formula.local_bottle_path).to eq(bottle.realpath)
end
end
context "when given an alias" do
subject(:formula) { described_class.factory("foo") }
let(:alias_dir) { CoreTap.instance.alias_dir }
let(:alias_path) { alias_dir/"foo" }
before do
alias_dir.mkpath
FileUtils.ln_s formula_path, alias_path
end
it "returns a Formula" do
expect(formula).to be_a(Formula)
end
it "calling #alias_path on the returned Formula returns the alias path" do
expect(formula.alias_path).to eq(alias_path)
end
end
context "with installed Formula" do
before do
# don't try to load/fetch gcc/glibc
allow(DevelopmentTools).to receive_messages(needs_libc_formula?: false, needs_compiler_formula?: false)
end
let(:installed_formula) { described_class.factory(formula_path) }
let(:installer) { FormulaInstaller.new(installed_formula) }
it "returns a Formula when given a rack" do
installer.fetch
installer.install
f = described_class.from_rack(installed_formula.rack)
expect(f).to be_a(Formula)
end
it "returns a Formula when given a Keg" do
installer.fetch
installer.install
keg = Keg.new(installed_formula.prefix)
f = described_class.from_keg(keg)
expect(f).to be_a(Formula)
end
end
context "when migrating from a Tap" do
let(:tap) { Tap.fetch("homebrew", "foo") }
let(:another_tap) { Tap.fetch("homebrew", "bar") }
let(:tap_migrations_path) { tap.path/"tap_migrations.json" }
let(:another_tap_formula_path) { another_tap.path/"Formula/#{formula_name}.rb" }
before do
tap.path.mkpath
another_tap_formula_path.dirname.mkpath
another_tap_formula_path.write formula_content
end
after do
FileUtils.rm_rf tap.path
FileUtils.rm_rf another_tap.path
end
it "returns a Formula that has gone through a tap migration into homebrew/core" do
tap_migrations_path.write <<~EOS
{
"#{formula_name}": "homebrew/core"
}
EOS
formula = described_class.factory("#{tap}/#{formula_name}")
expect(formula).to be_a(Formula)
expect(formula.tap).to eq(CoreTap.instance)
expect(formula.path).to eq(formula_path)
end
it "returns a Formula that has gone through a tap migration into another tap" do
tap_migrations_path.write <<~EOS
{
"#{formula_name}": "#{another_tap}"
}
EOS
formula = described_class.factory("#{tap}/#{formula_name}")
expect(formula).to be_a(Formula)
expect(formula.tap).to eq(another_tap)
expect(formula.path).to eq(another_tap_formula_path)
end
end
context "when loading from Tap" do
let(:tap) { Tap.fetch("homebrew", "foo") }
let(:another_tap) { Tap.fetch("homebrew", "bar") }
let(:formula_path) { tap.path/"Formula/#{formula_name}.rb" }
let(:alias_name) { "bar" }
let(:alias_dir) { tap.alias_dir }
let(:alias_path) { alias_dir/alias_name }
before do
alias_dir.mkpath
FileUtils.ln_s formula_path, alias_path
end
it "returns a Formula when given a name" do
expect(described_class.factory(formula_name)).to be_a(Formula)
end
it "returns a Formula from an Alias path" do
expect(described_class.factory(alias_name)).to be_a(Formula)
end
it "returns a Formula from a fully qualified Alias path" do
expect(described_class.factory("#{tap.name}/#{alias_name}")).to be_a(Formula)
end
it "raises an error when the Formula cannot be found" do
expect do
described_class.factory("#{tap}/not_existed_formula")
end.to raise_error(TapFormulaUnavailableError)
end
it "returns a Formula when given a fully qualified name" do
expect(described_class.factory("#{tap}/#{formula_name}")).to be_a(Formula)
end
it "raises an error if a Formula is in multiple Taps" do
(another_tap.path/"Formula").mkpath
(another_tap.path/"Formula/#{formula_name}.rb").write formula_content
expect do
described_class.factory(formula_name)
end.to raise_error(TapFormulaAmbiguityError)
end
end
context "when loading from the API" do
def formula_json_contents(extra_items = {})
{
formula_name => {
"desc" => "testball",
"homepage" => "https://example.com",
"license" => "MIT",
"revision" => 0,
"version_scheme" => 0,
"versions" => { "stable" => "0.1" },
"urls" => {
"stable" => {
"url" => "file://#{TEST_FIXTURE_DIR}/tarballs/testball-0.1.tbz",
"tag" => nil,
"revision" => nil,
},
},
"bottle" => {
"stable" => {
"rebuild" => 0,
"root_url" => "file://#{bottle_dir}",
"files" => {
Utils::Bottles.tag.to_s => {
"cellar" => ":any",
"url" => "file://#{bottle_dir}/#{formula_name}",
"sha256" => "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97",
},
},
},
},
"keg_only_reason" => {
"reason" => ":provided_by_macos",
"explanation" => "",
},
"build_dependencies" => ["build_dep"],
"dependencies" => ["dep"],
"test_dependencies" => ["test_dep"],
"recommended_dependencies" => ["recommended_dep"],
"optional_dependencies" => ["optional_dep"],
"uses_from_macos" => ["uses_from_macos_dep"],
"requirements" => [
{
"name" => "xcode",
"cask" => nil,
"download" => nil,
"version" => "1.0",
"contexts" => ["build"],
},
],
"conflicts_with" => ["conflicting_formula"],
"conflicts_with_reasons" => ["it does"],
"link_overwrite" => ["bin/abc"],
"caveats" => "example caveat string\n/$HOME\n$HOMEBREW_PREFIX",
"service" => {
"name" => { macos: "custom.launchd.name", linux: "custom.systemd.name" },
"run" => ["$HOMEBREW_PREFIX/opt/formula_name/bin/beanstalkd", "test"],
"run_type" => "immediate",
"working_dir" => "/$HOME",
},
"ruby_source_path" => "Formula/#{formula_name}.rb",
"ruby_source_checksum" => { "sha256" => "ABCDEFGHIJKLMNOPQRSTUVWXYZ" },
}.merge(extra_items),
}
end
let(:deprecate_json) do
{
"deprecation_date" => "2022-06-15",
"deprecation_reason" => "repo_archived",
}
end
let(:disable_json) do
{
"disable_date" => "2022-06-15",
"disable_reason" => "requires something else",
}
end
let(:variations_json) do
{
"variations" => {
Utils::Bottles.tag.to_s => {
"dependencies" => ["dep", "variations_dep"],
},
},
}
end
let(:older_macos_variations_json) do
{
"variations" => {
Utils::Bottles.tag.to_s => {
"dependencies" => ["uses_from_macos_dep"],
},
},
}
end
let(:linux_variations_json) do
{
"variations" => {
"x86_64_linux" => {
"dependencies" => ["dep", "uses_from_macos_dep"],
},
},
}
end
before do
ENV.delete("HOMEBREW_NO_INSTALL_FROM_API")
# avoid unnecessary network calls
allow(Homebrew::API::Formula).to receive_messages(all_aliases: {}, all_renames: {})
allow(CoreTap.instance).to receive(:tap_migrations).and_return({})
# don't try to load/fetch gcc/glibc
allow(DevelopmentTools).to receive_messages(needs_libc_formula?: false, needs_compiler_formula?: false)
end
it "returns a Formula when given a name" do
allow(Homebrew::API::Formula).to receive(:all_formulae).and_return formula_json_contents
formula = described_class.factory(formula_name)
expect(formula).to be_a(Formula)
expect(formula.keg_only_reason.reason).to eq :provided_by_macos
expect(formula.declared_deps.count).to eq 6
if OS.mac?
expect(formula.deps.count).to eq 5
else
expect(formula.deps.count).to eq 6
end
expect(formula.requirements.count).to eq 1
req = formula.requirements.first
expect(req).to be_an_instance_of XcodeRequirement
expect(req.version).to eq "1.0"
expect(req.tags).to eq [:build]
expect(formula.conflicts.map(&:name)).to include "conflicting_formula"
expect(formula.conflicts.map(&:reason)).to include "it does"
expect(formula.class.link_overwrite_paths).to include "bin/abc"
expect(formula.caveats).to eq "example caveat string\n#{Dir.home}\n#{HOMEBREW_PREFIX}"
expect(formula).to be_a_service
expect(formula.service.command).to eq(["#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd", "test"])
expect(formula.service.run_type).to eq(:immediate)
expect(formula.service.working_dir).to eq(Dir.home)
expect(formula.plist_name).to eq("custom.launchd.name")
expect(formula.service_name).to eq("custom.systemd.name")
expect(formula.ruby_source_checksum.hexdigest).to eq("abcdefghijklmnopqrstuvwxyz")
expect do
formula.install
end.to raise_error("Cannot build from source from abstract formula.")
end
it "returns a deprecated Formula when given a name" do
allow(Homebrew::API::Formula).to receive(:all_formulae).and_return formula_json_contents(deprecate_json)
formula = described_class.factory(formula_name)
expect(formula).to be_a(Formula)
expect(formula.deprecated?).to be true
expect do
formula.install
end.to raise_error("Cannot build from source from abstract formula.")
end
it "returns a disabled Formula when given a name" do
allow(Homebrew::API::Formula).to receive(:all_formulae).and_return formula_json_contents(disable_json)
formula = described_class.factory(formula_name)
expect(formula).to be_a(Formula)
expect(formula.disabled?).to be true
expect do
formula.install
end.to raise_error("Cannot build from source from abstract formula.")
end
it "returns a Formula with variations when given a name", :needs_macos do
allow(Homebrew::API::Formula).to receive(:all_formulae).and_return formula_json_contents(variations_json)
formula = described_class.factory(formula_name)
expect(formula).to be_a(Formula)
expect(formula.declared_deps.count).to eq 7
expect(formula.deps.count).to eq 6
expect(formula.deps.map(&:name).include?("variations_dep")).to be true
expect(formula.deps.map(&:name).include?("uses_from_macos_dep")).to be false
end
it "returns a Formula without duplicated deps and uses_from_macos with variations on Linux", :needs_linux do
allow(Homebrew::API::Formula)
.to receive(:all_formulae).and_return formula_json_contents(linux_variations_json)
formula = described_class.factory(formula_name)
expect(formula).to be_a(Formula)
expect(formula.declared_deps.count).to eq 6
expect(formula.deps.count).to eq 6
expect(formula.deps.map(&:name).include?("uses_from_macos_dep")).to be true
end
it "returns a Formula with the correct uses_from_macos dep on older macOS", :needs_macos do
allow(Homebrew::API::Formula)
.to receive(:all_formulae).and_return formula_json_contents(older_macos_variations_json)
formula = described_class.factory(formula_name)
expect(formula).to be_a(Formula)
expect(formula.declared_deps.count).to eq 6
expect(formula.deps.count).to eq 5
expect(formula.deps.map(&:name).include?("uses_from_macos_dep")).to be true
end
end
end
specify "::from_contents" do
expect(described_class.from_contents(formula_name, formula_path, formula_content)).to be_a(Formula)
end
describe "::to_rack" do
alias_matcher :exist, :be_exist
let(:rack_path) { HOMEBREW_CELLAR/formula_name }
context "when the Rack does not exist" do
it "returns the Rack" do
expect(described_class.to_rack(formula_name)).to eq(rack_path)
end
end
context "when the Rack exists" do
before do
rack_path.mkpath
end
it "returns the Rack" do
expect(described_class.to_rack(formula_name)).to eq(rack_path)
end
end
it "raises an error if the Formula is not available" do
expect do
described_class.to_rack("a/b/#{formula_name}")
end.to raise_error(TapFormulaUnavailableError)
end
end
describe "::core_path" do
it "returns the path to a Formula in the core tap" do
name = "foo-bar"
expect(described_class.core_path(name))
.to eq(Pathname.new("#{HOMEBREW_LIBRARY}/Taps/homebrew/homebrew-core/Formula/#{name}.rb"))
end
end
describe "::convert_to_string_or_symbol" do
it "returns the original string if it doesn't start with a colon" do
expect(described_class.convert_to_string_or_symbol("foo")).to eq "foo"
end
it "returns a symbol if the original string starts with a colon" do
expect(described_class.convert_to_string_or_symbol(":foo")).to eq :foo
end
end
describe "::loader_for" do
context "when given a relative path with two slashes" do
it "returns a `FromPathLoader`" do
mktmpdir.cd do
FileUtils.mkdir "Formula"
FileUtils.touch "Formula/gcc.rb"
expect(described_class.loader_for("./Formula/gcc.rb")).to be_a Formulary::FromPathLoader
end
end
end
context "when given a tapped name" do
it "returns a `FromTapLoader`" do
expect(described_class.loader_for("homebrew/core/gcc")).to be_a Formulary::FromTapLoader
end
end
context "when not using the API" do
before do
ENV["HOMEBREW_NO_INSTALL_FROM_API"] = "1"
end
context "when a formula is migrated" do
let(:token) { "foo" }
let(:core_tap) { CoreTap.instance }
let(:core_cask_tap) { CoreCaskTap.instance }
let(:tap_migrations) do
{
token => new_tap.name,
}
end
before do
old_tap.path.mkpath
new_tap.path.mkpath
(old_tap.path/"tap_migrations.json").write tap_migrations.to_json
end
context "to a cask in the default tap" do
let(:old_tap) { core_tap }
let(:new_tap) { core_cask_tap }
let(:cask_file) { new_tap.cask_dir/"#{token}.rb" }
before do
new_tap.cask_dir.mkpath
FileUtils.touch cask_file
end
it "warn only once" do
expect do
described_class.loader_for(token)
end.to output(
a_string_including("Warning: Formula #{token} was renamed to #{new_tap}/#{token}.").once,
).to_stderr
end
end
context "to the default tap" do
let(:old_tap) { core_cask_tap }
let(:new_tap) { core_tap }
let(:formula_file) { new_tap.formula_dir/"#{token}.rb" }
before do
new_tap.formula_dir.mkpath
FileUtils.touch formula_file
end
it "does not warn when loading the short token" do
expect do
described_class.loader_for(token)
end.not_to output.to_stderr
end
it "does not warn when loading the full token in the default tap" do
expect do
described_class.loader_for("#{new_tap}/#{token}")
end.not_to output.to_stderr
end
it "warns when loading the full token in the old tap" do
expect do
described_class.loader_for("#{old_tap}/#{token}")
end.to output(
a_string_including("Formula #{old_tap}/#{token} was renamed to #{token}.").once,
).to_stderr
end
# FIXME
# context "when there is an infinite tap migration loop" do
# before do
# (new_tap.path/"tap_migrations.json").write({
# token => old_tap.name,
# }.to_json)
# end
#
# it "stops recursing" do
# expect do
# described_class.loader_for("#{new_tap}/#{token}")
# end.not_to output.to_stderr
# end
# end
end
end
end
end
end