Files
brew/Library/Homebrew/test/cmd/update-report_spec.rb
T

728 lines
29 KiB
Ruby

# typed: true
# frozen_string_literal: true
require "cmd/update-report"
require "formula_versions"
require "yaml"
require "cmd/shared_examples/args_parse"
require "cmd/shared_examples/reinstall_pkgconf_if_needed"
RSpec.describe Homebrew::Cmd::UpdateReport do
it_behaves_like "parseable arguments"
it_behaves_like "reinstall_pkgconf_if_needed"
it "links to the donations section" do
allow(Homebrew::Settings).to receive(:read).with("donationmessage").and_return("false")
expect { described_class.new([]).donation_message }
.to output(include("https://github.com/Homebrew/brew#-donations")).to_stdout
end
# Simulate update.sh after a redirected fetch has advanced HEAD and origin/<branch>.
def setup_redirected_tap(name)
tap = Tap.fetch("allowed", name)
tap.path.mkpath
system "git", "-C", tap.path.to_s, "init"
system "git", "-C", tap.path.to_s, "remote", "add", "origin", "https://allowed.example/homebrew-#{name}"
(tap.path/"before").write("before")
system "git", "-C", tap.path.to_s, "add", "--all"
system "git", "-C", tap.path.to_s, "commit", "-q", "-m", "before"
before_revision = Utils.popen_read("git", "-C", tap.path.to_s, "rev-parse", "HEAD").chomp
(tap.path/"after").write("after")
system "git", "-C", tap.path.to_s, "add", "--all"
system "git", "-C", tap.path.to_s, "commit", "-q", "-m", "after"
branch = Utils.popen_read("git", "-C", tap.path.to_s, "symbolic-ref", "--short", "HEAD").chomp
system "git", "-C", tap.path.to_s, "update-ref", "refs/remotes/origin/#{branch}", "HEAD"
[tap, before_revision, branch]
end
def run_quiet_update_report
with_env(
HOMEBREW_UPDATE_BEFORE: "abc",
HOMEBREW_UPDATE_AFTER: "abc",
HOMEBREW_UPDATE_TEST: "1",
) { described_class.new(["--quiet"]).run }
end
it "copies update revisions for redirected tap names" do
redirected_remotes_file = mktmpdir/"redirected-remotes"
redirected_remotes_file.write("/tmp/homebrew-foo\thttps://github.com/new/homebrew-foo.git\n")
tap = instance_double(Tap, repository_var_suffix: "_OLD_HOMEBREW_FOO")
allow(Tap).to receive(:from_path).with("/tmp/homebrew-foo").and_return(tap)
allow(tap).to receive(:apply_redirected_remote!)
.with("https://github.com/new/homebrew-foo.git", quiet: true) do
allow(tap).to receive(:repository_var_suffix).and_return("_NEW_HOMEBREW_FOO")
end
allow(Homebrew::EnvConfig).to receive_messages(disable_load_formula?: true, no_install_from_api?: true)
update_report = described_class.new(["--quiet"])
allow(update_report).to receive(:tap_or_untap_core_taps_if_necessary)
with_env(
HOMEBREW_REDIRECTED_REMOTES_FILE: redirected_remotes_file.to_s,
HOMEBREW_UPDATE_BEFORE: "abc",
HOMEBREW_UPDATE_AFTER: "abc",
HOMEBREW_UPDATE_BEFORE_OLD_HOMEBREW_FOO: "123",
HOMEBREW_UPDATE_AFTER_OLD_HOMEBREW_FOO: "456",
) do
update_report.run
expect(ENV.fetch("HOMEBREW_UPDATE_BEFORE_NEW_HOMEBREW_FOO")).to eq("123")
expect(ENV.fetch("HOMEBREW_UPDATE_AFTER_NEW_HOMEBREW_FOO")).to eq("456")
end
end
it "refuses an off-allowlist redirect and rolls the tap back to its pre-update revision" do
tap, before_revision, branch = setup_redirected_tap("foo")
redirected_remotes_file = mktmpdir/"redirected-remotes"
redirected_remotes_file.write("#{tap.path}\thttps://attacker.example/homebrew-foo\n")
allow(Homebrew::EnvConfig).to receive_messages(allowed_taps: "https://allowed.example/homebrew-foo",
disable_load_formula?: true, no_install_from_api?: true)
update_report = described_class.new(["--quiet"])
with_env(
"HOMEBREW_REDIRECTED_REMOTES_FILE" => redirected_remotes_file.to_s,
"HOMEBREW_UPDATE_BEFORE" => "abc",
"HOMEBREW_UPDATE_AFTER" => "abc",
"HOMEBREW_UPDATE_BEFORE#{tap.repository_var_suffix}" => before_revision,
) do
expect { update_report.run }.to raise_error(SystemExit)
end
expect(Utils.popen_read("git", "-C", tap.path, "rev-parse", "HEAD").chomp).to eq(before_revision)
expect(Utils.popen_read("git", "-C", tap.path, "rev-parse", "refs/remotes/origin/#{branch}").chomp)
.to eq(before_revision)
expect(Utils.popen_read("git", "-C", tap.path, "config", "remote.origin.url").chomp)
.to eq("https://allowed.example/homebrew-foo")
ensure
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"allowed"
end
it "rolls back every denied tap when several off-allowlist redirects are in the file" do
foo_tap, foo_before, foo_branch = setup_redirected_tap("foo")
bar_tap, bar_before, bar_branch = setup_redirected_tap("bar")
redirected_remotes_file = mktmpdir/"redirected-remotes"
redirected_remotes_file.write(
"#{foo_tap.path}\thttps://attacker.example/homebrew-foo\n" \
"#{bar_tap.path}\thttps://attacker.example/homebrew-bar\n",
)
allow(Homebrew::EnvConfig).to receive_messages(
allowed_taps: "https://allowed.example/homebrew-foo https://allowed.example/homebrew-bar",
disable_load_formula?: true,
no_install_from_api?: true,
)
update_report = described_class.new(["--quiet"])
with_env(
"HOMEBREW_REDIRECTED_REMOTES_FILE" => redirected_remotes_file.to_s,
"HOMEBREW_UPDATE_BEFORE" => "abc",
"HOMEBREW_UPDATE_AFTER" => "abc",
"HOMEBREW_UPDATE_BEFORE#{foo_tap.repository_var_suffix}" => foo_before,
"HOMEBREW_UPDATE_BEFORE#{bar_tap.repository_var_suffix}" => bar_before,
) do
expect { update_report.run }.to raise_error(SystemExit)
end
expect(Utils.popen_read("git", "-C", foo_tap.path, "rev-parse", "HEAD").chomp).to eq(foo_before)
expect(Utils.popen_read("git", "-C", foo_tap.path, "rev-parse", "refs/remotes/origin/#{foo_branch}").chomp)
.to eq(foo_before)
expect(Utils.popen_read("git", "-C", bar_tap.path, "rev-parse", "HEAD").chomp).to eq(bar_before)
expect(Utils.popen_read("git", "-C", bar_tap.path, "rev-parse", "refs/remotes/origin/#{bar_branch}").chomp)
.to eq(bar_before)
ensure
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"allowed"
end
it "rolls back the remote-tracking ref for a denied redirect when HEAD is detached" do
tap, before_revision, branch = setup_redirected_tap("foo")
# Detached HEAD makes `symbolic-ref HEAD` empty, so the rollback must fall back to origin/HEAD.
system "git", "-C", tap.path.to_s, "symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/#{branch}"
system "git", "-C", tap.path.to_s, "checkout", "-q", "--detach", "HEAD"
redirected_remotes_file = mktmpdir/"redirected-remotes"
redirected_remotes_file.write("#{tap.path}\thttps://attacker.example/homebrew-foo\n")
allow(Homebrew::EnvConfig).to receive_messages(allowed_taps: "https://allowed.example/homebrew-foo",
disable_load_formula?: true, no_install_from_api?: true)
update_report = described_class.new(["--quiet"])
with_env(
"HOMEBREW_REDIRECTED_REMOTES_FILE" => redirected_remotes_file.to_s,
"HOMEBREW_UPDATE_BEFORE" => "abc",
"HOMEBREW_UPDATE_AFTER" => "abc",
"HOMEBREW_UPDATE_BEFORE#{tap.repository_var_suffix}" => before_revision,
) do
expect { update_report.run }.to raise_error(SystemExit)
end
expect(Utils.popen_read("git", "-C", tap.path, "rev-parse", "HEAD").chomp).to eq(before_revision)
expect(Utils.popen_read("git", "-C", tap.path, "rev-parse", "refs/remotes/origin/#{branch}").chomp)
.to eq(before_revision)
ensure
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"allowed"
end
it "migrates supported Caskroom Ruby and internal JSON metadata to JSON for all users" do
caskroom = mktmpdir/"Caskroom"
rb_caskfile = caskroom/"local-caffeine/.metadata/1.0/20250101000000.000/Casks/local-caffeine.rb"
json_caskfile = rb_caskfile.sub_ext(".json")
uninstall_flight_caskfile =
caskroom/"with-uninstall-preflight/.metadata/1.0/20250101000000.000/Casks/with-uninstall-preflight.rb"
receiptless_caskfile =
caskroom/"receiptless-cask/.metadata/1.0/20250101000000.000/Casks/receiptless-cask.rb"
receiptless_json_caskfile = receiptless_caskfile.sub_ext(".json")
internal_json_caskfile = caskroom/"api-cask/.metadata/1.0/20250101000000.000/Casks/api-cask.internal.json"
api_caskfile = internal_json_caskfile.dirname/"api-cask.json"
rb_caskfile.dirname.mkpath
rb_caskfile.write <<~RUBY
cask "local-caffeine" do
version "1.0"
sha256 :no_check
url "https://example.com/local-caffeine.zip"
name "Local Caffeine"
homepage "https://example.com/local-caffeine"
app "Caffeine.app"
end
RUBY
uninstall_flight_caskfile.dirname.mkpath
uninstall_flight_caskfile.write <<~RUBY
cask "with-uninstall-preflight" do
version "1.0"
sha256 :no_check
url "https://example.com/with-uninstall-preflight.zip"
name "With Uninstall Preflight"
homepage "https://example.com/with-uninstall-preflight"
app "With Uninstall Preflight.app"
uninstall_preflight do
# do nothing
end
end
RUBY
receiptless_caskfile.dirname.mkpath
receiptless_caskfile.write <<~RUBY
cask "receiptless-cask" do
version "1.0"
sha256 :no_check
url "https://example.com/receiptless-cask.zip"
name "Receipt-less Cask"
homepage "https://example.com/receiptless-cask"
app "Receipt-less Cask.app"
uninstall quit: "com.example.receiptless-cask"
zap trash: "~/Library/Preferences/com.example.receiptless-cask.plist"
end
RUBY
(caskroom/"local-caffeine/.metadata/INSTALL_RECEIPT.json").write JSON.pretty_generate({
"source" => { "version" => "1.0" },
"uninstall_artifacts" => [{ "app" => ["Caffeine.app"] }],
})
internal_json_caskfile.dirname.mkpath
internal_json_caskfile.write JSON.pretty_generate({
"homepage" => "https://example.com/api-cask",
"names" => ["API Cask"],
"raw_artifacts" => [[":app", ["API Cask.app"]]],
"sha256" => "no_check",
"url_args" => ["https://example.com/api-cask.zip"],
"version" => "1.0",
})
(caskroom/"api-cask/.metadata/INSTALL_RECEIPT.json").write JSON.pretty_generate({
"source" => { "version" => "1.0" },
"uninstall_artifacts" => [{ "app" => ["API Cask.app"] }],
})
allow(Cask::Caskroom).to receive(:path).and_return(caskroom)
allow(Homebrew::EnvConfig).to receive_messages(developer?: false, disable_load_formula?: true,
no_install_from_api?: true)
run_quiet_update_report
loaded_cask = Cask::CaskLoader.load_from_installed_caskfile(json_caskfile)
loaded_receiptless_cask = Cask::CaskLoader.load_from_installed_caskfile(receiptless_json_caskfile)
loaded_api_cask = Cask::CaskLoader.load_from_installed_caskfile(api_caskfile)
expect([
rb_caskfile.exist?,
JSON.parse(json_caskfile.read).keys,
loaded_cask.version.to_s,
loaded_cask.artifacts.grep(Cask::Artifact::App).count,
uninstall_flight_caskfile.exist?,
uninstall_flight_caskfile.sub_ext(".json").exist?,
Cask::CaskLoader.load_from_installed_caskfile(uninstall_flight_caskfile).uninstall_flight_blocks?,
receiptless_caskfile.exist?,
JSON.parse(receiptless_json_caskfile.read).fetch("artifacts"),
loaded_receiptless_cask.artifacts_list(uninstall_only: true),
internal_json_caskfile.exist?,
JSON.parse(api_caskfile.read).keys,
loaded_api_cask.loaded_from_internal_api?,
loaded_api_cask.artifacts.grep(Cask::Artifact::App).count,
]).to eq([
false,
[],
"1.0",
1,
true,
false,
true,
false,
[
{ "uninstall" => [{ "quit" => "com.example.receiptless-cask" }] },
{ "app" => ["Receipt-less Cask.app"] },
{ "zap" => [{ "trash" => "~/Library/Preferences/com.example.receiptless-cask.plist" }] },
],
[
{ uninstall: [{ quit: "com.example.receiptless-cask" }] },
{ app: ["Receipt-less Cask.app"] },
{ zap: [{ trash: "~/Library/Preferences/com.example.receiptless-cask.plist" }] },
],
false,
[],
false,
1,
])
end
it "repairs migrated Cask metadata that differs" do
caskroom = mktmpdir/"Caskroom"
caskfile = caskroom/"mismatched/.metadata/1.0/20250101000000.000/Casks/mismatched.rb"
json_caskfile = caskfile.sub_ext(".json")
caskfile.dirname.mkpath
caskfile.write <<~RUBY
cask "mismatched" do
version "2.0"
sha256 :no_check
url "https://example.com/mismatched.zip"
name "Mismatched"
homepage "https://example.com/mismatched"
app "Mismatched.app"
end
RUBY
(caskroom/"mismatched/.metadata/INSTALL_RECEIPT.json").write JSON.pretty_generate({
"source" => { "version" => "1.0" },
"uninstall_artifacts" => [{ "app" => ["Wrong.app"] }],
})
allow(Cask::Caskroom).to receive(:path).and_return(caskroom)
allow(Homebrew::EnvConfig).to receive_messages(developer?: false, disable_load_formula?: true,
no_install_from_api?: true)
2.times { run_quiet_update_report }
migrated_cask = Cask::CaskLoader.load_from_installed_caskfile(json_caskfile)
expect([
caskfile.exist?,
JSON.parse(json_caskfile.read),
migrated_cask.version.to_s,
migrated_cask.artifacts_list(uninstall_only: true),
]).to eq([
false,
{
"version" => "2.0",
"artifacts" => [{ "app" => ["Mismatched.app"] }],
},
"2.0",
[{ app: ["Mismatched.app"] }],
])
end
it "repairs existing JSON metadata once" do
caskroom = mktmpdir/"Caskroom"
caskfile = caskroom/"stubbed/.metadata/1.0/20250101000000.000/Casks/stubbed.json"
caskfile.dirname.mkpath
caskfile.write("{}")
allow(Cask::Caskroom).to receive(:path).and_return(caskroom)
allow(Homebrew::EnvConfig).to receive_messages(developer?: false, disable_load_formula?: true,
no_install_from_api?: true)
allow(Homebrew::API).to receive(:cask_token?).with("stubbed").and_return(true)
expect(Homebrew::API::Cask).to receive(:cask_json).once.with("stubbed").and_return({
"artifacts" => [{ "app" => ["Stubbed.app"] }],
})
2.times { run_quiet_update_report }
expect(JSON.parse(caskfile.read)).to eq({
"artifacts" => [{ "app" => ["Stubbed.app"] }],
})
end
it "migrates the aged Caskroom fixture eras and preserves their artifacts" do
# The fixture holds installed metadata byte-for-byte as older Homebrew versions wrote it.
caskroom = Cask::Caskroom.path
FileUtils.cp_r TEST_FIXTURE_DIR/"cask/aged_caskroom", caskroom
pre_receipt_rb_caskfile = caskroom/"pre-receipt-rb/.metadata/1.0/20250101000000.000/Casks/pre-receipt-rb.rb"
pre_receipt_rb_json_caskfile = pre_receipt_rb_caskfile.sub_ext(".json")
pre_receipt_stubbed_caskfile =
caskroom/"pre-receipt-stubbed/.metadata/1.0/20250101000000.000/Casks/pre-receipt-stubbed.json"
receipt_era_stub_caskfile =
caskroom/"receipt-era-stub/.metadata/1.0/20250101000000.000/Casks/receipt-era-stub.json"
internal_json_caskfile =
caskroom/"internal-json/.metadata/1.0/20250101000000.000/Casks/internal-json.internal.json"
migrated_internal_caskfile = internal_json_caskfile.dirname/"internal-json.json"
uninstall_flight_caskfile =
caskroom/"uninstall-flight-block/.metadata/1.0/20250101000000.000/Casks/uninstall-flight-block.rb"
allow(Homebrew::API).to receive(:cask_token?).with("pre-receipt-stubbed").and_return(true)
expect(Homebrew::API::Cask).to receive(:cask_json).once.with("pre-receipt-stubbed").and_return({
"artifacts" => [{ "app" => ["Pre Receipt Stubbed.app"] }],
})
described_class.new(["--quiet"]).migrate_caskroom_caskfiles_to_json
migrated_rb_cask = Cask::CaskLoader.load_from_installed_caskfile(pre_receipt_rb_json_caskfile)
receipt_era_stub_cask = Cask::CaskLoader.load_from_installed_caskfile(receipt_era_stub_caskfile)
migrated_internal_cask = Cask::CaskLoader.load_from_installed_caskfile(migrated_internal_caskfile)
expect([
pre_receipt_rb_caskfile.exist?,
JSON.parse(pre_receipt_rb_json_caskfile.read),
migrated_rb_cask.version.to_s,
migrated_rb_cask.artifacts_list(uninstall_only: true),
JSON.parse(pre_receipt_stubbed_caskfile.read),
JSON.parse(receipt_era_stub_caskfile.read),
receipt_era_stub_cask.artifacts_list(uninstall_only: true),
internal_json_caskfile.exist?,
JSON.parse(migrated_internal_caskfile.read),
migrated_internal_cask.artifacts_list(uninstall_only: true),
uninstall_flight_caskfile.exist?,
uninstall_flight_caskfile.sub_ext(".json").exist?,
Cask::CaskLoader.load_from_installed_caskfile(uninstall_flight_caskfile).uninstall_flight_blocks?,
]).to eq([
false,
{ "artifacts" => [{ "app" => ["Pre Receipt Rb.app"] }] },
"1.0",
[{ app: ["Pre Receipt Rb.app"] }],
{ "artifacts" => [{ "app" => ["Pre Receipt Stubbed.app"] }] },
{},
[{ app: ["Receipt Era Stub.app"] }],
false,
{},
[{ app: ["Internal JSON.app"] }],
true,
false,
true,
])
end
describe Reporter do
let(:tap) { CoreTap.instance }
let(:reporter_class) do
Class.new(described_class) do
def initialize(tap)
@tap = tap
ENV["HOMEBREW_UPDATE_BEFORE#{tap.repository_var_suffix}"] = "12345678"
ENV["HOMEBREW_UPDATE_AFTER#{tap.repository_var_suffix}"] = "abcdef00"
super
end
end
end
let(:reporter) { reporter_class.new(tap) }
let(:hub) { ReporterHub.new }
def perform_update(fixture_name = "")
allow(Formulary).to receive(:factory).and_return(instance_double(Formula, pkg_version: "1.0"))
allow(FormulaVersions).to receive(:new).and_return(instance_double(FormulaVersions, formula_at_revision: "2.0"))
diff = YAML.load_file("#{TEST_FIXTURE_DIR}/updater_fixture.yaml")[fixture_name]
allow(reporter).to receive(:diff).and_return(diff || "")
hub.add(reporter) if reporter.updated?
end
specify "without revision variable" do
ENV.delete_if { |k, _v| k.start_with? "HOMEBREW_UPDATE" }
expect do
described_class.new(tap)
end.to raise_error(Reporter::ReporterRevisionUnsetError)
end
specify "without any changes" do
perform_update
expect(hub).to be_empty
end
specify "without Formula changes" do
perform_update("update_git_diff_output_without_formulae_changes")
expect(hub.select_formula_or_cask(:M)).to be_empty
expect(hub.select_formula_or_cask(:A)).to be_empty
expect(hub.select_formula_or_cask(:D)).to be_empty
end
specify "with Formula changes" do
perform_update("update_git_diff_output_with_formulae_changes")
expect(hub.select_formula_or_cask(:M)).to eq(%w[xar yajl])
expect(hub.select_formula_or_cask(:A)).to eq(%w[antiword bash-completion ddrescue dict lua])
end
specify "with removed Formulae" do
perform_update("update_git_diff_output_with_removed_formulae")
expect(hub.select_formula_or_cask(:D)).to eq(%w[libgsasl])
end
specify "with changed file type" do
perform_update("update_git_diff_output_with_changed_filetype")
expect(hub.select_formula_or_cask(:M)).to eq(%w[elixir])
expect(hub.select_formula_or_cask(:A)).to eq(%w[libbson])
expect(hub.select_formula_or_cask(:D)).to eq(%w[libgsasl])
end
specify "with renamed Formula" do
allow(tap).to receive(:formula_renames).and_return("cv" => "progress")
perform_update("update_git_diff_output_with_formula_rename")
expect(hub.select_formula_or_cask(:A)).to be_empty
expect(hub.select_formula_or_cask(:D)).to be_empty
expect(hub.renamed_formulae).to eq([["cv", "progress"]])
end
context "when updating a Tap other than the core Tap" do
let(:tap) { Tap.fetch("foo", "bar") }
before do
(tap.path/"Formula").mkpath
end
after do
FileUtils.rm_r(tap.path.parent)
end
specify "with restructured Tap" do
perform_update("update_git_diff_output_with_restructured_tap")
expect(hub.select_formula_or_cask(:A)).to be_empty
expect(hub.select_formula_or_cask(:D)).to be_empty
expect(hub.renamed_formulae).to be_empty
end
specify "with renamed Formula and restructured Tap" do
allow(tap).to receive(:formula_renames).and_return("xchat" => "xchat2")
perform_update("update_git_diff_output_with_formula_rename_and_restructuring")
expect(hub.select_formula_or_cask(:A)).to be_empty
expect(hub.select_formula_or_cask(:D)).to be_empty
expect(hub.renamed_formulae).to eq([%w[foo/bar/xchat foo/bar/xchat2]])
end
specify "with simulated 'homebrew/php' restructuring" do
perform_update("update_git_diff_simulate_homebrew_php_restructuring")
expect(hub.select_formula_or_cask(:A)).to be_empty
expect(hub.select_formula_or_cask(:D)).to be_empty
expect(hub.renamed_formulae).to be_empty
end
specify "with Formula changes" do
perform_update("update_git_diff_output_with_tap_formulae_changes")
expect(hub.select_formula_or_cask(:A)).to eq(%w[foo/bar/lua])
expect(hub.select_formula_or_cask(:M)).to eq(%w[foo/bar/git])
expect(hub.renamed_formulae).to be_empty
end
specify "with formula migrated to cask in same tap" do
# Setup a tap with both formulae and casks
(tap.path/"Formula").mkpath
(tap.path/"Casks").mkpath
(tap.path/"tap_migrations.json").write <<~JSON
{ "old-formula": "foo/bar/new-cask" }
JSON
# Mock that the tap has a cask with the migration target name
allow(tap).to receive(:cask_tokens).and_return(["new-cask"])
reporter_instance = reporter_class.new(tap)
allow(reporter_instance).to receive(:report).and_return({ D: ["foo/bar/old-formula"] })
# Verify the migration would be detected as formula-to-cask migration
expect(tap.tap_migrations).to eq({ "old-formula" => "foo/bar/new-cask" })
expect(tap.cask_tokens).to include("new-cask")
end
end
describe "#ensure_trusted_tap_installed!" do
let(:other_tap) { Tap.fetch("foo", "bar") }
before { allow(other_tap).to receive(:installed?).and_return(false) }
it "recommends trusting just the migrated package then migrating a rename" do
expect(other_tap).not_to receive(:ensure_installed!)
expect { reporter.ensure_trusted_tap_installed!("oldfoo", "newfoo", other_tap) }
.to output(%r{brew trust foo/bar/newfoo.*brew migrate oldfoo}m).to_stderr
end
it "recommends a reinstall for an unchanged-name tap migration" do
expect { reporter.ensure_trusted_tap_installed!("foo", "foo", other_tap) }
.to output(/brew reinstall foo/).to_stderr
end
it "taps a trusted tap" do
allow(other_tap).to receive(:official?).and_return(true)
expect(other_tap).to receive(:ensure_installed!)
reporter.ensure_trusted_tap_installed!("foo", "foo", other_tap)
end
end
describe "#diff" do
context "when using the API" do
subject(:reporter) do
described_class.new(tap,
api_names_txt: Pathname("formula_names.txt"),
api_names_before_txt: Pathname("formula_names_before.txt"),
api_dir_prefix: HOMEBREW_CACHE/"api")
end
it "ignore lines that haven't changed" do
expect(Utils).to receive(:popen_read).and_return(<<~DIFF)
foo
+bar
-baz
DIFF
expect(reporter.diff).to eq(<<~DIFF.strip)
A api/bar.rb
D api/baz.rb
DIFF
end
it "handles moved lines" do
expect(Utils).to receive(:popen_read).and_return(<<~DIFF)
+baz
foo
+bar
+baz
-bar
-baz
DIFF
expect(reporter.diff).to eq(<<~DIFF.strip)
A api/baz.rb
DIFF
end
end
end
end
describe ReporterHub do
let(:hub) { described_class.new }
before do
ENV["HOMEBREW_NO_COLOR"] = "1"
allow(hub).to receive(:select_formula_or_cask).and_return([])
end
it "dumps new formulae report" do
allow(hub).to receive(:select_formula_or_cask).with(:A).and_return(["foo", "bar", "baz"])
allow(hub).to receive_messages(installed?: false, all_formula_json: [
{ "name" => "foo", "desc" => "foobly things" },
{ "name" => "baz", "desc" => "baz desc" },
])
expect { hub.dump }.to output(<<~EOS).to_stdout
==> New Formulae
bar
baz: baz desc
foo: foobly things
EOS
end
it "dumps new casks report" do
allow(hub).to receive(:select_formula_or_cask).with(:AC).and_return(["cask1", "cask2", "foo/tap/cask3"])
allow(hub).to receive_messages(cask_installed?: false, all_cask_json: [
{ "token" => "cask1", "desc" => "desc1" },
{ "token" => "cask3", "desc" => "desc3" },
])
allow(Cask::Caskroom).to receive(:any_casks_installed?).and_return(true)
expect { hub.dump }.to output(<<~EOS).to_stdout
==> New Casks
cask1: desc1
cask2
cask3
EOS
end
it "does not dump update details when HOMEBREW_AUTO_UPDATE_QUIET is set during auto-update" do
ENV["HOMEBREW_AUTO_UPDATE_QUIET"] = "1"
allow(hub).to receive(:select_formula_or_cask).with(:A).and_return(["foo"])
allow(hub).to receive(:installed?).and_return(false)
expect { hub.dump(auto_update: true) }.not_to output.to_stdout
end
it "dumps update details when HOMEBREW_AUTO_UPDATE_QUIET is set during an explicit update" do
ENV["HOMEBREW_AUTO_UPDATE_QUIET"] = "1"
allow(hub).to receive(:select_formula_or_cask).with(:A).and_return(["foo"])
allow(hub).to receive_messages(installed?: false, description: nil)
expect { hub.dump }.to output("==> New Formulae\nfoo\n").to_stdout
end
it "dumps deleted installed formulae and casks report" do
allow(hub).to receive(:select_formula_or_cask).with(:D).and_return(["baz", "foo", "bar"])
allow(hub).to receive(:installed?).with("baz").and_return(true)
allow(hub).to receive(:installed?).with("foo").and_return(true)
allow(hub).to receive(:installed?).with("bar").and_return(true)
allow(hub).to receive(:select_formula_or_cask).with(:A).and_return([])
allow(hub).to receive(:select_formula_or_cask).with(:DC).and_return(["cask2", "cask1"])
allow(hub).to receive(:cask_installed?).with("cask1").and_return(true)
allow(hub).to receive(:cask_installed?).with("cask2").and_return(true)
allow(Homebrew::SimulateSystem).to receive(:simulating_or_running_on_linux?).and_return(false)
expect { hub.dump }.to output(<<~EOS).to_stdout
==> Deleted Installed Formulae
bar
baz
foo
==> Deleted Installed Casks
cask1
cask2
EOS
end
it "dumps outdated formulae and casks report" do
allow(Formula).to receive(:installed).and_return([
instance_double(Formula, name: "foo", outdated?: true),
instance_double(Formula, name: "bar", outdated?: true),
])
allow(Cask::Caskroom).to receive(:casks).and_return([
instance_double(Cask::Cask, token: "baz", outdated?: true),
instance_double(Cask::Cask, token: "qux", outdated?: true),
])
expect { hub.dump }.to output(<<~EOS).to_stdout
==> Outdated Formulae
bar
foo
==> Outdated Casks
baz
qux
You have 2 outdated formulae and 2 outdated casks installed.
You can upgrade them with brew upgrade
or list them with brew outdated.
EOS
end
it "skips the outdated count when auto-updating before a zero-argument upgrade or outdated" do
ENV["HOMEBREW_AUTO_UPDATE_SKIP_OUTDATED"] = "1"
allow(Formula).to receive(:installed).and_return([
instance_double(Formula, name: "foo", outdated?: true),
])
allow(Cask::Caskroom).to receive(:casks).and_return([])
expect { hub.dump(auto_update: true) }.not_to output.to_stdout
end
it "prints nothing if there are no changes" do
allow(Formula).to receive(:installed).and_return([])
allow(Cask::Caskroom).to receive(:casks).and_return([])
expect { hub.dump }.not_to output.to_stdout
end
it "merges frozen report arrays" do
allow(hub).to receive(:select_formula_or_cask).and_call_original
first_reporter = instance_double(Reporter, report: { A: ["foo"].freeze })
second_reporter = instance_double(Reporter, report: { A: ["bar"] })
hub.add(first_reporter)
hub.add(second_reporter)
expect(hub.select_formula_or_cask(:A)).to eq(%w[foo bar])
end
end
end