brew/Library/Homebrew/test/cask/cask_spec.rb
Mike McQuaid bbdea29a0f
Deprecate installing casks/formulae from paths.
We've already disabled installing casks/formulae from URLs and we
regularly tell people not to install from paths so let's just deprecate
this behaviour entirely.

Even Homebrew developers do not need to work this way.
2024-09-26 20:25:07 +01:00

454 lines
16 KiB
Ruby

# frozen_string_literal: true
RSpec.describe Cask::Cask, :cask do
let(:cask) { described_class.new("versioned-cask") }
context "when multiple versions are installed" do
describe "#installed_version" do
context "when there are duplicate versions" do
it "uses the last unique version" do
allow(cask).to receive(:timestamped_versions).and_return([
["1.2.2", "0999"],
["1.2.3", "1000"],
["1.2.2", "1001"],
])
# Installed caskfile must exist to count as installed.
allow_any_instance_of(Pathname).to receive(:exist?).and_return(true)
expect(cask).to receive(:timestamped_versions)
expect(cask.installed_version).to eq("1.2.2")
end
end
end
end
describe "load" do
let(:tap_path) { CoreCaskTap.instance.path }
let(:file_dirname) { Pathname.new(__FILE__).dirname }
let(:relative_tap_path) { tap_path.relative_path_from(file_dirname) }
it "returns an instance of the Cask for the given token" do
c = Cask::CaskLoader.load("local-caffeine")
expect(c).to be_a(described_class)
expect(c.token).to eq("local-caffeine")
end
it "returns an instance of the Cask from a specific file location" do
c = Cask::CaskLoader.load("#{tap_path}/Casks/local-caffeine.rb")
expect(c).to be_a(described_class)
expect(c.token).to eq("local-caffeine")
end
it "returns an instance of the Cask from a JSON file" do
c = Cask::CaskLoader.load("#{TEST_FIXTURE_DIR}/cask/caffeine.json")
expect(c).to be_a(described_class)
expect(c.token).to eq("caffeine")
end
it "raises an error when failing to download a Cask from a URL", :needs_utils_curl, :no_api do
expect do
Cask::CaskLoader.load("file://#{tap_path}/Casks/notacask.rb")
end.to raise_error(MethodDeprecatedError)
end
it "returns an instance of the Cask from a relative file location" do
c = Cask::CaskLoader.load(relative_tap_path/"Casks/local-caffeine.rb")
expect(c).to be_a(described_class)
expect(c.token).to eq("local-caffeine")
end
it "uses exact match when loading by token" do
expect(Cask::CaskLoader.load("test-opera").token).to eq("test-opera")
expect(Cask::CaskLoader.load("test-opera-mail").token).to eq("test-opera-mail")
end
it "raises an error when attempting to load a Cask that doesn't exist" do
expect do
Cask::CaskLoader.load("notacask")
end.to raise_error(Cask::CaskUnavailableError)
end
end
describe "metadata" do
it "proposes a versioned metadata directory name for each instance" do
cask_token = "local-caffeine"
c = Cask::CaskLoader.load(cask_token)
metadata_timestamped_path = Cask::Caskroom.path.join(cask_token, ".metadata", c.version)
expect(c.metadata_versioned_path.to_s).to eq(metadata_timestamped_path.to_s)
end
end
describe "outdated" do
it "ignores the Casks that have auto_updates true (without --greedy)" do
c = Cask::CaskLoader.load("auto-updates")
expect(c).not_to be_outdated
expect(c.outdated_version).to be_nil
end
it "ignores the Casks that have version :latest (without --greedy)" do
c = Cask::CaskLoader.load("version-latest-string")
expect(c).not_to be_outdated
expect(c.outdated_version).to be_nil
end
describe "versioned casks" do
subject { cask.outdated_version }
let(:cask) { described_class.new("basic-cask") }
shared_examples "versioned casks" do |tap_version, expectations|
expectations.each do |installed_version, expected_output|
context "when version #{installed_version.inspect} is installed and the tap version is #{tap_version}" do
it {
allow(cask).to receive_messages(installed_version:,
version: Cask::DSL::Version.new(tap_version))
expect(cask).to receive(:outdated_version).and_call_original
expect(subject).to eq expected_output
}
end
end
end
describe "installed version is equal to tap version => not outdated" do
include_examples "versioned casks", "1.2.3",
"1.2.3" => nil
end
describe "installed version is different than tap version => outdated" do
include_examples "versioned casks", "1.2.4",
"1.2.3" => "1.2.3",
"1.2.4" => nil
end
end
describe ":latest casks" do
let(:cask) { described_class.new("basic-cask") }
shared_examples ":latest cask" do |greedy, outdated_sha, tap_version, expectations|
expectations.each do |installed_version, expected_output|
context "when versions #{installed_version} are installed and the " \
"tap version is #{tap_version}, #{"not " unless greedy}greedy " \
"and sha is #{"not " unless outdated_sha}outdated" do
subject { cask.outdated_version(greedy:) }
it {
allow(cask).to receive_messages(installed_version:,
version: Cask::DSL::Version.new(tap_version),
outdated_download_sha?: outdated_sha)
expect(cask).to receive(:outdated_version).and_call_original
expect(subject).to eq expected_output
}
end
end
end
describe ":latest version installed, :latest version in tap" do
include_examples ":latest cask", false, false, "latest",
"latest" => nil
include_examples ":latest cask", true, false, "latest",
"latest" => nil
include_examples ":latest cask", true, true, "latest",
"latest" => "latest"
end
describe "numbered version installed, :latest version in tap" do
include_examples ":latest cask", false, false, "latest",
"1.2.3" => nil
include_examples ":latest cask", true, false, "latest",
"1.2.3" => nil
include_examples ":latest cask", true, true, "latest",
"1.2.3" => "1.2.3"
end
describe "latest version installed, numbered version in tap" do
include_examples ":latest cask", false, false, "1.2.3",
"latest" => "latest"
include_examples ":latest cask", true, false, "1.2.3",
"latest" => "latest"
include_examples ":latest cask", true, true, "1.2.3",
"latest" => "latest"
end
end
end
describe "full_name" do
context "when it is a core cask" do
it "is the cask token" do
c = Cask::CaskLoader.load("local-caffeine")
expect(c.full_name).to eq("local-caffeine")
end
end
context "when it is from a non-core tap" do
it "returns the fully-qualified name of the cask" do
c = Cask::CaskLoader.load("third-party/tap/third-party-cask")
expect(c.full_name).to eq("third-party/tap/third-party-cask")
end
end
context "when it is from no known tap" do
it "returns the cask token" do
file = Tempfile.new(%w[tapless-cask .rb])
begin
cask_name = File.basename(file.path, ".rb")
file.write "cask '#{cask_name}'"
file.close
c = Cask::CaskLoader.load(file.path)
expect(c.full_name).to eq(cask_name)
ensure
file.close
file.unlink
end
end
end
end
describe "#artifacts_list" do
subject(:cask) { Cask::CaskLoader.load("many-artifacts") }
it "returns all artifacts when no options are given" do
expected_artifacts = [
{ uninstall_preflight: nil },
{ preflight: nil },
{ uninstall: [{
rmdir: "#{TEST_TMPDIR}/empty_directory_path",
trash: ["#{TEST_TMPDIR}/foo", "#{TEST_TMPDIR}/bar"],
}] },
{ pkg: ["ManyArtifacts/ManyArtifacts.pkg"] },
{ app: ["ManyArtifacts/ManyArtifacts.app"] },
{ uninstall_postflight: nil },
{ postflight: nil },
{ zap: [{
rmdir: ["~/Library/Caches/ManyArtifacts", "~/Library/Application Support/ManyArtifacts"],
trash: "~/Library/Logs/ManyArtifacts.log",
}] },
]
expect(cask.artifacts_list).to eq(expected_artifacts)
end
it "skips flight blocks when compact is true" do
expected_artifacts = [
{ uninstall: [{
rmdir: "#{TEST_TMPDIR}/empty_directory_path",
trash: ["#{TEST_TMPDIR}/foo", "#{TEST_TMPDIR}/bar"],
}] },
{ pkg: ["ManyArtifacts/ManyArtifacts.pkg"] },
{ app: ["ManyArtifacts/ManyArtifacts.app"] },
{ zap: [{
rmdir: ["~/Library/Caches/ManyArtifacts", "~/Library/Application Support/ManyArtifacts"],
trash: "~/Library/Logs/ManyArtifacts.log",
}] },
]
expect(cask.artifacts_list(compact: true)).to eq(expected_artifacts)
end
it "returns only uninstall artifacts when uninstall_only is true" do
expected_artifacts = [
{ uninstall_preflight: nil },
{ uninstall: [{
rmdir: "#{TEST_TMPDIR}/empty_directory_path",
trash: ["#{TEST_TMPDIR}/foo", "#{TEST_TMPDIR}/bar"],
}] },
{ app: ["ManyArtifacts/ManyArtifacts.app"] },
{ uninstall_postflight: nil },
{ zap: [{
rmdir: ["~/Library/Caches/ManyArtifacts", "~/Library/Application Support/ManyArtifacts"],
trash: "~/Library/Logs/ManyArtifacts.log",
}] },
]
expect(cask.artifacts_list(uninstall_only: true)).to eq(expected_artifacts)
end
it "skips flight blocks and returns only uninstall artifacts when compact and uninstall_only are true" do
expected_artifacts = [
{ uninstall: [{
rmdir: "#{TEST_TMPDIR}/empty_directory_path",
trash: ["#{TEST_TMPDIR}/foo", "#{TEST_TMPDIR}/bar"],
}] },
{ app: ["ManyArtifacts/ManyArtifacts.app"] },
{ zap: [{
rmdir: ["~/Library/Caches/ManyArtifacts", "~/Library/Application Support/ManyArtifacts"],
trash: "~/Library/Logs/ManyArtifacts.log",
}] },
]
expect(cask.artifacts_list(compact: true, uninstall_only: true)).to eq(expected_artifacts)
end
end
describe "#uninstall_flight_blocks?" do
matcher :have_uninstall_flight_blocks do
match do |actual|
actual.uninstall_flight_blocks? == true
end
end
it "returns true when there are uninstall_preflight blocks" do
cask = Cask::CaskLoader.load("with-uninstall-preflight")
expect(cask).to have_uninstall_flight_blocks
end
it "returns true when there are uninstall_postflight blocks" do
cask = Cask::CaskLoader.load("with-uninstall-postflight")
expect(cask).to have_uninstall_flight_blocks
end
it "returns false when there are only preflight blocks" do
cask = Cask::CaskLoader.load("with-preflight")
expect(cask).not_to have_uninstall_flight_blocks
end
it "returns false when there are only postflight blocks" do
cask = Cask::CaskLoader.load("with-postflight")
expect(cask).not_to have_uninstall_flight_blocks
end
it "returns false when there are no flight blocks" do
cask = Cask::CaskLoader.load("local-caffeine")
expect(cask).not_to have_uninstall_flight_blocks
end
end
describe "#to_h" do
let(:expected_json) { (TEST_FIXTURE_DIR/"cask/everything.json").read.strip }
context "when loaded from cask file" do
it "returns expected hash" do
allow(MacOS).to receive(:version).and_return(MacOSVersion.new("13"))
cask = Cask::CaskLoader.load("everything")
expect(cask.tap).to receive(:git_head).and_return("abcdef1234567890abcdef1234567890abcdef12")
hash = cask.to_h
expect(hash).to be_a(Hash)
expect(JSON.pretty_generate(hash)).to eq(expected_json)
end
end
context "when loaded from json file" do
it "returns expected hash" do
expect(Homebrew::API::Cask).not_to receive(:source_download)
hash = Cask::CaskLoader::FromAPILoader.new(
"everything", from_json: JSON.parse(expected_json)
).load(config: nil).to_h
expect(hash).to be_a(Hash)
expect(JSON.pretty_generate(hash)).to eq(expected_json)
end
end
end
describe "#to_hash_with_variations" do
let!(:original_macos_version) { MacOS.full_version.to_s }
let(:expected_versions_variations) do
<<~JSON
{
"monterey": {
"url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine/darwin/1.2.3/intel.zip"
},
"big_sur": {
"url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine/darwin/1.2.0/intel.zip",
"version": "1.2.0",
"sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
},
"arm64_big_sur": {
"url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine/darwin-arm64/1.2.0/arm.zip",
"version": "1.2.0",
"sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
},
"catalina": {
"url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine/darwin/1.0.0/intel.zip",
"version": "1.0.0",
"sha256": "1866dfa833b123bb8fe7fa7185ebf24d28d300d0643d75798bc23730af734216"
},
"mojave": {
"url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine/darwin/1.0.0/intel.zip",
"version": "1.0.0",
"sha256": "1866dfa833b123bb8fe7fa7185ebf24d28d300d0643d75798bc23730af734216"
}
}
JSON
end
let(:expected_sha256_variations) do
<<~JSON
{
"monterey": {
"url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel.zip",
"sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
},
"big_sur": {
"url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel.zip",
"sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
},
"catalina": {
"url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel.zip",
"sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
},
"mojave": {
"url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel.zip",
"sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
}
}
JSON
end
before do
# Use a more limited symbols list to shorten the variations hash
symbols = {
monterey: "12",
big_sur: "11",
catalina: "10.15",
mojave: "10.14",
}
stub_const("MacOSVersion::SYMBOLS", symbols)
# For consistency, always run on Monterey and ARM
MacOS.full_version = "12"
allow(Hardware::CPU).to receive(:type).and_return(:arm)
end
after do
MacOS.full_version = original_macos_version
end
it "returns the correct variations hash for a cask with multiple versions" do
c = Cask::CaskLoader.load("multiple-versions")
h = c.to_hash_with_variations
expect(h).to be_a(Hash)
expect(JSON.pretty_generate(h["variations"])).to eq expected_versions_variations.strip
end
it "returns the correct variations hash for a cask different sha256s on each arch" do
c = Cask::CaskLoader.load("sha256-arch")
h = c.to_hash_with_variations
expect(h).to be_a(Hash)
expect(JSON.pretty_generate(h["variations"])).to eq expected_sha256_variations.strip
end
# NOTE: The calls to `Cask.generating_hash!` and `Cask.generated_hash!`
# are not idempotent so they can only be used in one test.
it "returns the correct hash placeholders" do
described_class.generating_hash!
expect(described_class).to be_generating_hash
c = Cask::CaskLoader.load("placeholders")
h = c.to_hash_with_variations
described_class.generated_hash!
expect(described_class).not_to be_generating_hash
expect(h).to be_a(Hash)
expect(h["artifacts"].first[:binary].first).to eq "$APPDIR/some/path"
expect(h["caveats"]).to eq "$HOMEBREW_PREFIX and /$HOME\n"
end
end
end