# 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 context "without the API", :no_api 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 "errors when given a path but paths are disabled" do ENV["HOMEBREW_FORBID_PACKAGES_FROM_PATHS"] = "1" FileUtils.cp formula_path, HOMEBREW_TEMP temp_formula_path = HOMEBREW_TEMP/formula_path.basename expect do described_class.factory(temp_formula_path) ensure temp_formula_path.unlink end.to raise_error(FormulaUnavailableError) end it "returns a Formula when given a URL", :needs_utils_curl do formula = described_class.factory("file://#{formula_path}") expect(formula).to be_a(Formula) end it "errors when given a URL but paths are disabled" do ENV["HOMEBREW_FORBID_PACKAGES_FROM_PATHS"] = "1" expect do described_class.factory("file://#{formula_path}") end.to raise_error(FormulaUnavailableError) 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 end context "with 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 # avoid unnecessary network calls allow(Homebrew::API::Formula).to receive_messages(all_aliases: {}, all_renames: {}) allow(CoreTap.instance).to receive(:tap_migrations).and_return({}) allow(CoreCaskTap.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 context "with core tap migration renames" do let(:foo_tap) { Tap.fetch("homebrew", "foo") } before do allow(Homebrew::API::Formula).to receive(:all_formulae).and_return formula_json_contents foo_tap.path.mkpath end after do FileUtils.rm_rf foo_tap.path end it "returns the tap migration rename by old formula_name" do old_formula_name = "#{formula_name}-old" (foo_tap.path/"tap_migrations.json").write <<~JSON { "#{old_formula_name}": "homebrew/core/#{formula_name}" } JSON loader = described_class::FromNameLoader.try_new(old_formula_name) expect(loader).to be_a(described_class::FromAPILoader) expect(loader.name).to eq formula_name expect(loader.path).not_to exist end it "returns the tap migration rename by old full name" do old_formula_name = "#{formula_name}-old" (foo_tap.path/"tap_migrations.json").write <<~JSON { "#{old_formula_name}": "homebrew/core/#{formula_name}" } JSON loader = described_class::FromTapLoader.try_new("#{foo_tap}/#{old_formula_name}") expect(loader).to be_a(described_class::FromAPILoader) expect(loader.name).to eq formula_name expect(loader.path).not_to exist end end end context "when passed a URL" do it "raises an error when given an https URL" do expect do described_class.factory("https://brew.sh/foo.rb") end.to raise_error(UnsupportedInstallationMethod) end it "raises an error when given a bottle URL" do expect do described_class.factory("https://brew.sh/foo-1.0.arm64_catalina.bottle.tar.gz") end.to raise_error(UnsupportedInstallationMethod) end it "raises an error when given an ftp URL" do expect do described_class.factory("ftp://brew.sh/foo.rb") end.to raise_error(UnsupportedInstallationMethod) end it "raises an error when given an sftp URL" do expect do described_class.factory("sftp://brew.sh/foo.rb") end.to raise_error(UnsupportedInstallationMethod) end it "does not raise an error when given a file URL" do expect do described_class.factory("file://#{TEST_FIXTURE_DIR}/testball.rb") end.not_to raise_error(UnsupportedInstallationMethod) end end context "when passed ref with spaces" do it "raises a FormulaUnavailableError error" do expect do described_class.factory("foo bar") end.to raise_error(FormulaUnavailableError) 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`", :no_api do expect(described_class.loader_for("homebrew/core/gcc")).to be_a Formulary::FromTapLoader end end context "when not using the API", :no_api do 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 context "to a third-party tap" do let(:old_tap) { Tap.fetch("another", "foo") } let(:new_tap) { Tap.fetch("another", "bar") } let(:formula_file) { new_tap.formula_dir/"#{token}.rb" } before do new_tap.formula_dir.mkpath FileUtils.touch formula_file end after do FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"another" end # FIXME # It would be preferable not to print a warning when installing with the short token it "warns when loading the short token" do expect do described_class.loader_for(token) end.to output( a_string_including("Formula #{old_tap}/#{token} was renamed to #{new_tap}/#{token}.").once, ).to_stderr end it "does not warn when loading the full token in the new 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 #{new_tap}/#{token}.").once, ).to_stderr end end end end end end