Merge pull request #21632 from mmatus3/nightshift/api-contract-verify

Add API contract verification tests for FormulaStruct and CaskStruct
This commit is contained in:
Rylan Polster
2026-02-26 20:32:26 +00:00
committed by GitHub
2 changed files with 312 additions and 0 deletions
@@ -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 = {