mirror of
https://github.com/Homebrew/brew.git
synced 2026-08-12 22:29:27 +04:00
Merge pull request #21632 from mmatus3/nightshift/api-contract-verify
Add API contract verification tests for FormulaStruct and CaskStruct
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "api"
|
||||
|
||||
RSpec.describe Homebrew::API::CaskStruct do
|
||||
describe "::from_hash" do
|
||||
it "constructs a valid struct from a hash with all field types" do
|
||||
hash = {
|
||||
"sha256" => "abc123",
|
||||
"version" => "1.0.0",
|
||||
"ruby_source_checksum" => { sha256: "def456" },
|
||||
"names" => ["Test Cask"],
|
||||
"desc" => "A test cask",
|
||||
"homepage" => "https://example.com",
|
||||
"auto_updates" => true,
|
||||
"languages" => ["en"],
|
||||
"url_args" => ["https://example.com/file.dmg"],
|
||||
"url_kwargs" => { verified: "example.com/" },
|
||||
"conflicts_with_args" => { cask: ["other-cask"] },
|
||||
"depends_on_args" => { macos: ">= :catalina" },
|
||||
"container_args" => { type: :zip },
|
||||
"deprecate_args" => { date: "2025-01-01", because: :discontinued },
|
||||
"raw_artifacts" => [[:app, ["Test.app"], {}, nil]],
|
||||
"raw_caveats" => "Requires restart.",
|
||||
}
|
||||
|
||||
struct = described_class.from_hash(hash)
|
||||
|
||||
expect(struct.sha256).to eq("abc123")
|
||||
expect(struct.version).to eq("1.0.0")
|
||||
expect(struct.names).to eq(["Test Cask"])
|
||||
expect(struct.desc).to eq("A test cask")
|
||||
expect(struct.homepage).to eq("https://example.com")
|
||||
expect(struct.auto_updates).to be(true)
|
||||
expect(struct.languages).to eq(["en"])
|
||||
end
|
||||
|
||||
it "ignores unknown/extra keys" do
|
||||
hash = {
|
||||
"sha256" => "abc123",
|
||||
"version" => "1.0.0",
|
||||
"ruby_source_checksum" => { sha256: "def456" },
|
||||
"totally_unknown_key" => "should be ignored",
|
||||
"another_unknown" => 42,
|
||||
}
|
||||
|
||||
expect { described_class.from_hash(hash) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe "predicate methods" do
|
||||
it "defaults all predicates to false for a minimal struct" do
|
||||
struct = described_class.new(
|
||||
sha256: "abc123",
|
||||
version: "1.0.0",
|
||||
ruby_source_checksum: { sha256: "def456" },
|
||||
)
|
||||
|
||||
Homebrew::API::CaskStruct::PREDICATES.each do |predicate|
|
||||
expect(struct.send(:"#{predicate}?")).to be(false),
|
||||
"expected #{predicate}? to default to false"
|
||||
end
|
||||
end
|
||||
|
||||
it "returns true when the corresponding _present field is set" do
|
||||
present_fields = Homebrew::API::CaskStruct::PREDICATES.to_h do |predicate|
|
||||
[:"#{predicate}_present", true]
|
||||
end
|
||||
|
||||
struct = described_class.new(
|
||||
sha256: "abc123",
|
||||
version: "1.0.0",
|
||||
ruby_source_checksum: { sha256: "def456" },
|
||||
**present_fields,
|
||||
)
|
||||
|
||||
Homebrew::API::CaskStruct::PREDICATES.each do |predicate|
|
||||
expect(struct.send(:"#{predicate}?")).to be(true),
|
||||
"expected #{predicate}? to be true"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#artifacts" do
|
||||
it "replaces placeholders in artifact arguments" do
|
||||
struct = described_class.new(
|
||||
sha256: "abc123",
|
||||
version: "1.0.0",
|
||||
ruby_source_checksum: { sha256: "def456" },
|
||||
raw_artifacts: [[:app, ["#{HOMEBREW_CASK_APPDIR_PLACEHOLDER}/Test.app"], {}, nil]],
|
||||
)
|
||||
|
||||
result = struct.artifacts(appdir: "/Applications")
|
||||
|
||||
expect(result).to eq([[:app, ["/Applications/Test.app"], {}, nil]])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#caveats" do
|
||||
it "replaces placeholders in caveats string" do
|
||||
struct = described_class.new(
|
||||
sha256: "abc123",
|
||||
version: "1.0.0",
|
||||
ruby_source_checksum: { sha256: "def456" },
|
||||
raw_caveats: "Installed to #{HOMEBREW_PREFIX_PLACEHOLDER}/bin",
|
||||
)
|
||||
|
||||
result = struct.caveats(appdir: "/Applications")
|
||||
|
||||
expect(result).to eq("Installed to #{HOMEBREW_PREFIX}/bin")
|
||||
end
|
||||
|
||||
it "returns nil when raw_caveats is nil" do
|
||||
struct = described_class.new(
|
||||
sha256: "abc123",
|
||||
version: "1.0.0",
|
||||
ruby_source_checksum: { sha256: "def456" },
|
||||
)
|
||||
|
||||
expect(struct.caveats(appdir: "/Applications")).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -131,6 +131,195 @@ RSpec.describe Homebrew::API::FormulaStruct do
|
||||
end
|
||||
end
|
||||
|
||||
describe "predicate methods" do
|
||||
it "defaults all predicates to false when not set" do
|
||||
struct = described_class.new(
|
||||
desc: "test",
|
||||
homepage: "https://example.com",
|
||||
license: "MIT",
|
||||
ruby_source_checksum: "abc123",
|
||||
stable_version: "1.0.0",
|
||||
)
|
||||
|
||||
Homebrew::API::FormulaStruct::PREDICATES.each do |predicate|
|
||||
expect(struct.send(:"#{predicate}?")).to be(false),
|
||||
"expected #{predicate}? to default to false"
|
||||
end
|
||||
end
|
||||
|
||||
it "returns true when the corresponding _present field is set" do
|
||||
present_fields = Homebrew::API::FormulaStruct::PREDICATES.to_h do |predicate|
|
||||
[:"#{predicate}_present", true]
|
||||
end
|
||||
|
||||
struct = described_class.new(
|
||||
desc: "test",
|
||||
homepage: "https://example.com",
|
||||
license: "MIT",
|
||||
ruby_source_checksum: "abc123",
|
||||
stable_version: "1.0.0",
|
||||
**present_fields,
|
||||
)
|
||||
|
||||
Homebrew::API::FormulaStruct::PREDICATES.each do |predicate|
|
||||
expect(struct.send(:"#{predicate}?")).to be(true),
|
||||
"expected #{predicate}? to be true"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "::deserialize" do
|
||||
it "reconstructs a struct from a serialized hash with bottle info" do
|
||||
bottle_tag = Utils::Bottles::Tag.from_symbol(:arm64_sequoia)
|
||||
hash = {
|
||||
"desc" => "test formula",
|
||||
"homepage" => "https://example.com",
|
||||
"license" => "MIT",
|
||||
"ruby_source_checksum" => "abc123",
|
||||
"stable_version" => "1.0.0",
|
||||
"bottle_checksum" => "checksum1",
|
||||
"bottle_tag" => ":arm64_sequoia",
|
||||
"bottle_cellar" => ":any",
|
||||
}
|
||||
|
||||
struct = described_class.deserialize(hash, bottle_tag:)
|
||||
|
||||
expect(struct.bottle?).to be(true)
|
||||
expect(struct.bottle_checksums).to eq([{ cellar: :any, arm64_sequoia: "checksum1" }])
|
||||
end
|
||||
|
||||
it "sets bottle_present to false when no bottle_checksum is present" do
|
||||
bottle_tag = Utils::Bottles::Tag.from_symbol(:arm64_sequoia)
|
||||
hash = {
|
||||
"desc" => "test formula",
|
||||
"homepage" => "https://example.com",
|
||||
"license" => "MIT",
|
||||
"ruby_source_checksum" => "abc123",
|
||||
"stable_version" => "1.0.0",
|
||||
}
|
||||
|
||||
struct = described_class.deserialize(hash, bottle_tag:)
|
||||
|
||||
expect(struct.bottle?).to be(false)
|
||||
expect(struct.bottle_checksums).to eq([])
|
||||
end
|
||||
|
||||
it "sets predicate _present fields from _args presence" do
|
||||
bottle_tag = Utils::Bottles::Tag.from_symbol(:arm64_sequoia)
|
||||
hash = {
|
||||
"desc" => "test formula",
|
||||
"homepage" => "https://example.com",
|
||||
"license" => "MIT",
|
||||
"ruby_source_checksum" => "abc123",
|
||||
"stable_version" => "1.0.0",
|
||||
"deprecate_args" => { ":date" => "2025-01-01", ":because" => "discontinued" },
|
||||
"keg_only_args" => [":versioned_formula"],
|
||||
}
|
||||
|
||||
struct = described_class.deserialize(hash, bottle_tag:)
|
||||
|
||||
expect(struct.deprecate?).to be(true)
|
||||
expect(struct.keg_only?).to be(true)
|
||||
expect(struct.disable?).to be(false)
|
||||
end
|
||||
|
||||
it "formats _url_args into [String, Hash] pairs" do
|
||||
bottle_tag = Utils::Bottles::Tag.from_symbol(:arm64_sequoia)
|
||||
hash = {
|
||||
"desc" => "test formula",
|
||||
"homepage" => "https://example.com",
|
||||
"license" => "MIT",
|
||||
"ruby_source_checksum" => "abc123",
|
||||
"stable_version" => "1.0.0",
|
||||
"stable_url_args" => ["https://example.com/foo-1.0.tar.gz"],
|
||||
}
|
||||
|
||||
struct = described_class.deserialize(hash, bottle_tag:)
|
||||
|
||||
expect(struct.stable?).to be(true)
|
||||
expect(struct.stable_url_args).to eq(["https://example.com/foo-1.0.tar.gz", {}])
|
||||
end
|
||||
|
||||
it "formats uses_from_macos into arg pairs" do
|
||||
bottle_tag = Utils::Bottles::Tag.from_symbol(:arm64_sequoia)
|
||||
hash = {
|
||||
"desc" => "test formula",
|
||||
"homepage" => "https://example.com",
|
||||
"license" => "MIT",
|
||||
"ruby_source_checksum" => "abc123",
|
||||
"stable_version" => "1.0.0",
|
||||
"stable_url_args" => ["https://example.com/foo-1.0.tar.gz"],
|
||||
"stable_uses_from_macos" => [["zlib"]],
|
||||
}
|
||||
|
||||
struct = described_class.deserialize(hash, bottle_tag:)
|
||||
|
||||
expect(struct.stable_uses_from_macos).to eq([["zlib", {}]])
|
||||
end
|
||||
|
||||
it "formats service_args into arg pairs" do
|
||||
bottle_tag = Utils::Bottles::Tag.from_symbol(:arm64_sequoia)
|
||||
hash = {
|
||||
"desc" => "test formula",
|
||||
"homepage" => "https://example.com",
|
||||
"license" => "MIT",
|
||||
"ruby_source_checksum" => "abc123",
|
||||
"stable_version" => "1.0.0",
|
||||
"service_args" => [[":run_type", ":immediate"]],
|
||||
}
|
||||
|
||||
struct = described_class.deserialize(hash, bottle_tag:)
|
||||
|
||||
expect(struct.service?).to be(true)
|
||||
expect(struct.service_args).to eq([[:run_type, :immediate]])
|
||||
end
|
||||
|
||||
it "formats conflicts into arg pairs" do
|
||||
bottle_tag = Utils::Bottles::Tag.from_symbol(:arm64_sequoia)
|
||||
hash = {
|
||||
"desc" => "test formula",
|
||||
"homepage" => "https://example.com",
|
||||
"license" => "MIT",
|
||||
"ruby_source_checksum" => "abc123",
|
||||
"stable_version" => "1.0.0",
|
||||
"conflicts" => [["other-formula"]],
|
||||
}
|
||||
|
||||
struct = described_class.deserialize(hash, bottle_tag:)
|
||||
|
||||
expect(struct.conflicts).to eq([["other-formula", {}]])
|
||||
end
|
||||
end
|
||||
|
||||
describe "serialize/deserialize round-trip" do
|
||||
it "reconstructs an equivalent struct after serialize then deserialize", :needs_macos do
|
||||
bottle_tag = Utils::Bottles::Tag.from_symbol(:arm64_sequoia)
|
||||
|
||||
original = described_class.new(
|
||||
desc: "round-trip test",
|
||||
homepage: "https://example.com",
|
||||
license: "MIT",
|
||||
ruby_source_checksum: "abc123",
|
||||
stable_version: "1.0.0",
|
||||
stable_present: true,
|
||||
stable_url_args: ["https://example.com/foo-1.0.tar.gz", {}],
|
||||
stable_dependencies: ["dep1", { "dep2" => :build }],
|
||||
stable_uses_from_macos: [["zlib", {}]],
|
||||
bottle_present: true,
|
||||
bottle_checksums: [{ cellar: :any, arm64_sequoia: "checksum1" }],
|
||||
conflicts: [["other-formula", {}]],
|
||||
revision: 2,
|
||||
aliases: ["foo-alias"],
|
||||
post_install_defined: true,
|
||||
)
|
||||
|
||||
serialized = original.serialize(bottle_tag:)
|
||||
restored = described_class.deserialize(serialized, bottle_tag:)
|
||||
|
||||
expect(restored).to eq(original)
|
||||
end
|
||||
end
|
||||
|
||||
describe "::deep_compact_blank" do
|
||||
it "removes blank values from nested hashes and arrays" do
|
||||
input = {
|
||||
|
||||
Reference in New Issue
Block a user