livecheck: allow inheriting from a formula/cask

This commit is contained in:
Sam Ford 2021-07-19 11:21:29 -04:00
parent bdc3391a13
commit ddde0f7589
No known key found for this signature in database
GPG Key ID: 95209E46C7FFDEFE
7 changed files with 382 additions and 20 deletions

View File

@ -18,6 +18,8 @@ class Livecheck
def initialize(formula_or_cask)
@formula_or_cask = formula_or_cask
@referenced_cask_name = nil
@referenced_formula_name = nil
@regex = nil
@skip = false
@skip_msg = nil
@ -25,6 +27,42 @@ class Livecheck
@url = nil
end
# Sets the `@referenced_cask_name` instance variable to the provided `String`
# or returns the `@referenced_cask_name` instance variable when no argument
# is provided. Inherited livecheck values from the referenced cask
# (e.g. regex) can be overridden in the livecheck block.
#
# @param cask_name [String] name of cask to inherit livecheck info from
# @return [String, nil]
def cask(cask_name = nil)
case cask_name
when nil
@referenced_cask_name
when String
@referenced_cask_name = cask_name
else
raise TypeError, "Livecheck#cask expects a String"
end
end
# Sets the `@referenced_formula_name` instance variable to the provided
# `String` or returns the `@referenced_formula_name` instance variable when
# no argument is provided. Inherited livecheck values from the referenced
# formula (e.g. regex) can be overridden in the livecheck block.
#
# @param formula_name [String] name of formula to inherit livecheck info from
# @return [String, nil]
def formula(formula_name = nil)
case formula_name
when nil
@referenced_formula_name
when String
@referenced_formula_name = formula_name
else
raise TypeError, "Livecheck#formula expects a String"
end
end
# Sets the `@regex` instance variable to the provided `Regexp` or returns the
# `@regex` instance variable when no argument is provided.
#
@ -109,6 +147,8 @@ class Livecheck
# @return [Hash]
def to_hash
{
"cask" => @referenced_cask_name,
"formula" => @referenced_formula_name,
"regex" => @regex,
"skip" => @skip,
"skip_msg" => @skip_msg,

View File

@ -9,6 +9,8 @@ require "ruby-progressbar"
require "uri"
module Homebrew
# rubocop:disable Metrics/ModuleLength
# The {Livecheck} module consists of methods used by the `brew livecheck`
# command. These methods print the requested livecheck information
# for formulae.
@ -82,6 +84,74 @@ module Homebrew
end
end
# Resolve formula/cask references in `livecheck` blocks to a final formula
# or cask.
sig {
params(
formula_or_cask: T.any(Formula, Cask::Cask),
first_formula_or_cask: T.any(Formula, Cask::Cask),
references: T::Array[T.any(Formula, Cask::Cask)],
full_name: T::Boolean,
debug: T::Boolean,
).returns(T.nilable(T::Array[T.untyped]))
}
def resolve_livecheck_reference(
formula_or_cask,
first_formula_or_cask = formula_or_cask,
references = [],
full_name: false,
debug: false
)
# Check the livecheck block for a formula or cask reference
livecheck = formula_or_cask.livecheck
livecheck_formula = livecheck.formula
livecheck_cask = livecheck.cask
return [nil, references] if livecheck_formula.blank? && livecheck_cask.blank?
# Load the referenced formula or cask
referenced_formula_or_cask = if livecheck_formula
Formulary.factory(livecheck_formula)
elsif livecheck_cask
Cask::CaskLoader.load(livecheck_cask)
end
# Error if a `livecheck` block references a formula/cask that was already
# referenced (or itself)
if referenced_formula_or_cask == first_formula_or_cask ||
referenced_formula_or_cask == formula_or_cask ||
references.include?(referenced_formula_or_cask)
if debug
# Print the chain of references for debugging
puts "Reference Chain:"
puts formula_or_cask_name(first_formula_or_cask, full_name: full_name)
references << referenced_formula_or_cask
references.each do |ref_formula_or_cask|
puts formula_or_cask_name(ref_formula_or_cask, full_name: full_name)
end
end
raise "Circular formula/cask reference encountered"
end
references << referenced_formula_or_cask
# Check the referenced formula/cask for a reference
next_referenced_formula_or_cask, next_references = resolve_livecheck_reference(
referenced_formula_or_cask,
first_formula_or_cask,
references,
full_name: full_name,
debug: debug,
)
# Returning references along with the final referenced formula/cask
# allows us to print the chain of references in the debug output
[
next_referenced_formula_or_cask || referenced_formula_or_cask,
next_references,
]
end
# Executes the livecheck logic for each formula/cask in the
# `formulae_and_casks_to_check` array and prints the results.
sig {
@ -139,6 +209,7 @@ module Homebrew
)
end
# rubocop:disable Metrics/BlockLength
formulae_checked = formulae_and_casks_to_check.map.with_index do |formula_or_cask, i|
formula = formula_or_cask if formula_or_cask.is_a?(Formula)
cask = formula_or_cask if formula_or_cask.is_a?(Cask::Cask)
@ -146,6 +217,9 @@ module Homebrew
use_full_name = full_name || ambiguous_names.include?(formula_or_cask)
name = formula_or_cask_name(formula_or_cask, full_name: use_full_name)
referenced_formula_or_cask, livecheck_references =
resolve_livecheck_reference(formula_or_cask, full_name: use_full_name, debug: debug)
if debug && i.positive?
puts <<~EOS
@ -156,7 +230,17 @@ module Homebrew
puts
end
skip_info = SkipConditions.skip_information(formula_or_cask, full_name: use_full_name, verbose: verbose)
# Check skip conditions for a referenced formula/cask
if referenced_formula_or_cask
skip_info = SkipConditions.referenced_skip_information(
referenced_formula_or_cask,
name,
full_name: use_full_name,
verbose: verbose,
)
end
skip_info ||= SkipConditions.skip_information(formula_or_cask, full_name: use_full_name, verbose: verbose)
if skip_info.present?
next skip_info if json
@ -188,7 +272,9 @@ module Homebrew
else
version_info = latest_version(
formula_or_cask,
json: json, full_name: use_full_name, verbose: verbose, debug: debug,
referenced_formula_or_cask: referenced_formula_or_cask,
livecheck_references: livecheck_references,
json: json, full_name: use_full_name, verbose: verbose, debug: debug
)
version_info[:latest] if version_info.present?
end
@ -262,6 +348,7 @@ module Homebrew
nil
end
end
# rubocop:enable Metrics/BlockLength
puts "No newer upstream versions." if newer_only && !has_a_newer_upstream_version && !debug && !json
@ -444,27 +531,40 @@ module Homebrew
# the version information. Returns nil if a latest version couldn't be found.
sig {
params(
formula_or_cask: T.any(Formula, Cask::Cask),
json: T::Boolean,
full_name: T::Boolean,
verbose: T::Boolean,
debug: T::Boolean,
formula_or_cask: T.any(Formula, Cask::Cask),
referenced_formula_or_cask: T.nilable(T.any(Formula, Cask::Cask)),
livecheck_references: T::Array[T.any(Formula, Cask::Cask)],
json: T::Boolean,
full_name: T::Boolean,
verbose: T::Boolean,
debug: T::Boolean,
).returns(T.nilable(Hash))
}
def latest_version(formula_or_cask, json: false, full_name: false, verbose: false, debug: false)
def latest_version(
formula_or_cask,
referenced_formula_or_cask: nil,
livecheck_references: [],
json: false, full_name: false, verbose: false, debug: false
)
formula = formula_or_cask if formula_or_cask.is_a?(Formula)
cask = formula_or_cask if formula_or_cask.is_a?(Cask::Cask)
has_livecheckable = formula_or_cask.livecheckable?
livecheck = formula_or_cask.livecheck
livecheck_url = livecheck.url
livecheck_regex = livecheck.regex
livecheck_strategy = livecheck.strategy
referenced_livecheck = referenced_formula_or_cask&.livecheck
livecheck_url_string = livecheck_url_to_string(livecheck_url, formula_or_cask)
livecheck_url = livecheck.url || referenced_livecheck&.url
livecheck_regex = livecheck.regex || referenced_livecheck&.regex
livecheck_strategy = livecheck.strategy || referenced_livecheck&.strategy
livecheck_strategy_block = livecheck.strategy_block || referenced_livecheck&.strategy_block
livecheck_url_string = livecheck_url_to_string(
livecheck_url,
referenced_formula_or_cask || formula_or_cask,
)
urls = [livecheck_url_string] if livecheck_url_string
urls ||= checkable_urls(formula_or_cask)
urls ||= checkable_urls(referenced_formula_or_cask || formula_or_cask)
if debug
if formula
@ -474,8 +574,18 @@ module Homebrew
puts "Cask: #{cask_name(formula_or_cask, full_name: full_name)}"
end
puts "Livecheckable?: #{has_livecheckable ? "Yes" : "No"}"
livecheck_references.each do |ref_formula_or_cask|
case ref_formula_or_cask
when Formula
puts "Formula Ref: #{formula_name(ref_formula_or_cask, full_name: full_name)}"
when Cask::Cask
puts "Cask Ref: #{cask_name(ref_formula_or_cask, full_name: full_name)}"
end
end
end
# rubocop:disable Metrics/BlockLength
urls.each_with_index do |original_url, i|
if debug
puts
@ -499,7 +609,7 @@ module Homebrew
livecheck_strategy: livecheck_strategy,
url_provided: livecheck_url.present?,
regex_provided: livecheck_regex.present?,
block_provided: livecheck.strategy_block.present?,
block_provided: livecheck_strategy_block.present?,
)
strategy = Strategy.from_symbol(livecheck_strategy) || strategies.first
strategy_name = livecheck_strategy_names[strategy]
@ -514,7 +624,7 @@ module Homebrew
end
if livecheck_strategy.present?
if livecheck_strategy == :page_match && (livecheck_regex.blank? && livecheck.strategy_block.blank?)
if livecheck_strategy == :page_match && (livecheck_regex.blank? && livecheck_strategy_block.blank?)
odebug "#{strategy_name} strategy requires a regex or block"
next
elsif livecheck_url.blank?
@ -529,7 +639,7 @@ module Homebrew
next if strategy.blank?
strategy_data = begin
strategy.find_versions(url, livecheck_regex, cask: cask, &livecheck.strategy_block)
strategy.find_versions(url, livecheck_regex, cask: cask, &livecheck_strategy_block)
rescue ArgumentError => e
raise unless e.message.include?("unknown keyword: cask")
@ -584,6 +694,17 @@ module Homebrew
if json && verbose
version_info[:meta] = {}
if livecheck_references.present?
version_info[:meta][:references] = livecheck_references.map do |ref_formula_or_cask|
case ref_formula_or_cask
when Formula
{ formula: formula_name(ref_formula_or_cask, full_name: full_name) }
when Cask::Cask
{ cask: cask_name(ref_formula_or_cask, full_name: full_name) }
end
end
end
version_info[:meta][:url] = {}
version_info[:meta][:url][:symbol] = livecheck_url if livecheck_url.is_a?(Symbol) && livecheck_url_string
version_info[:meta][:url][:original] = original_url
@ -599,8 +720,10 @@ module Homebrew
return version_info
end
# rubocop:enable Metrics/BlockLength
nil
end
end
# rubocop:enable Metrics/ModuleLength
end

View File

@ -201,6 +201,54 @@ module Homebrew
{}
end
# Skip conditions for formulae/casks referenced in a `livecheck` block
# are treated differently than normal. We only respect certain skip
# conditions (returning the related hash) and others are treated as
# errors.
sig {
params(
livecheck_formula_or_cask: T.any(Formula, Cask::Cask),
original_formula_or_cask_name: String,
full_name: T::Boolean,
verbose: T::Boolean,
).returns(T.nilable(Hash))
}
def referenced_skip_information(
livecheck_formula_or_cask,
original_formula_or_cask_name,
full_name: false,
verbose: false
)
skip_info = SkipConditions.skip_information(
livecheck_formula_or_cask,
full_name: full_name,
verbose: verbose,
)
return if skip_info.blank?
referenced_name = Livecheck.formula_or_cask_name(livecheck_formula_or_cask, full_name: full_name)
referenced_type = case livecheck_formula_or_cask
when Formula
:formula
when Cask::Cask
:cask
end
if skip_info[:status] != "error" &&
!(skip_info[:status] == "skipped" && livecheck_formula_or_cask.livecheck.skip?)
error_msg_end = if skip_info[:status] == "skipped"
"automatically skipped"
else
"skipped as #{skip_info[:status]}"
end
raise "Referenced #{referenced_type} (#{referenced_name}) is #{error_msg_end}"
end
skip_info[referenced_type] = original_formula_or_cask_name
skip_info
end
# Prints default livecheck output in relation to skip conditions.
sig { params(skip_hash: Hash).void }
def print_skip_information(skip_hash)

View File

@ -50,6 +50,10 @@ module RuboCop
skip = find_every_method_call_by_name(livecheck_node, :skip).first
return if skip.present?
formula_node = find_every_method_call_by_name(livecheck_node, :formula).first
cask_node = find_every_method_call_by_name(livecheck_node, :cask).first
return if formula_node.present? || cask_node.present?
livecheck_url = find_every_method_call_by_name(livecheck_node, :url).first
return if livecheck_url.present?

View File

@ -44,6 +44,15 @@ describe Homebrew::Livecheck do
RUBY
end
describe "::resolve_livecheck_reference" do
context "when a formula/cask has a livecheck block without formula/cask methods" do
it "returns [nil, []]" do
expect(livecheck.resolve_livecheck_reference(f)).to eq([nil, []])
expect(livecheck.resolve_livecheck_reference(c)).to eq([nil, []])
end
end
end
describe "::formula_name" do
it "returns the name of the formula" do
expect(livecheck.formula_name(f)).to eq("test")

View File

@ -264,7 +264,7 @@ describe Homebrew::Livecheck::SkipConditions do
}
end
describe "::skip_conditions" do
describe "::skip_information" do
context "when a formula without a livecheckable is deprecated" do
it "skips" do
expect(skip_conditions.skip_information(formulae[:deprecated]))
@ -293,21 +293,21 @@ describe Homebrew::Livecheck::SkipConditions do
end
end
context "when a formula has a GitHub Gist stable URL" do
context "when a formula without a livecheckable has a GitHub Gist stable URL" do
it "skips" do
expect(skip_conditions.skip_information(formulae[:gist]))
.to eq(status_hashes[:formula][:gist])
end
end
context "when a formula has a Google Code Archive stable URL" do
context "when a formula without a livecheckable has a Google Code Archive stable URL" do
it "skips" do
expect(skip_conditions.skip_information(formulae[:google_code_archive]))
.to eq(status_hashes[:formula][:google_code_archive])
end
end
context "when a formula has an Internet Archive stable URL" do
context "when a formula without a livecheckable has an Internet Archive stable URL" do
it "skips" do
expect(skip_conditions.skip_information(formulae[:internet_archive]))
.to eq(status_hashes[:formula][:internet_archive])
@ -364,6 +364,108 @@ describe Homebrew::Livecheck::SkipConditions do
end
end
describe "::referenced_skip_information" do
let(:original_name) { "original" }
context "when a formula without a livecheckable is deprecated" do
it "errors" do
expect { skip_conditions.referenced_skip_information(formulae[:deprecated], original_name) }
.to raise_error(RuntimeError, "Referenced formula (test_deprecated) is skipped as deprecated")
end
end
context "when a formula without a livecheckable is disabled" do
it "errors" do
expect { skip_conditions.referenced_skip_information(formulae[:disabled], original_name) }
.to raise_error(RuntimeError, "Referenced formula (test_disabled) is skipped as disabled")
end
end
context "when a formula without a livecheckable is versioned" do
it "errors" do
expect { skip_conditions.referenced_skip_information(formulae[:versioned], original_name) }
.to raise_error(RuntimeError, "Referenced formula (test@0.0.1) is skipped as versioned")
end
end
context "when a formula is HEAD-only and not installed" do
it "skips " do
expect(skip_conditions.referenced_skip_information(formulae[:head_only], original_name))
.to eq(status_hashes[:formula][:head_only].merge({ formula: original_name }))
end
end
context "when a formula without a livecheckable has a GitHub Gist stable URL" do
it "errors" do
expect { skip_conditions.referenced_skip_information(formulae[:gist], original_name) }
.to raise_error(RuntimeError, "Referenced formula (test_gist) is automatically skipped")
end
end
context "when a formula without a livecheckable has a Google Code Archive stable URL" do
it "errors" do
expect { skip_conditions.referenced_skip_information(formulae[:google_code_archive], original_name) }
.to raise_error(RuntimeError, "Referenced formula (test_google_code_archive) is automatically skipped")
end
end
context "when a formula without a livecheckable has an Internet Archive stable URL" do
it "errors" do
expect { skip_conditions.referenced_skip_information(formulae[:internet_archive], original_name) }
.to raise_error(RuntimeError, "Referenced formula (test_internet_archive) is automatically skipped")
end
end
context "when a formula has a `livecheck` block containing `skip`" do
it "skips" do
expect(skip_conditions.referenced_skip_information(formulae[:skip], original_name))
.to eq(status_hashes[:formula][:skip].merge({ formula: original_name }))
expect(skip_conditions.referenced_skip_information(formulae[:skip_with_message], original_name))
.to eq(status_hashes[:formula][:skip_with_message].merge({ formula: original_name }))
end
end
context "when a cask without a livecheckable is discontinued" do
it "errors" do
expect { skip_conditions.referenced_skip_information(casks[:discontinued], original_name) }
.to raise_error(RuntimeError, "Referenced cask (test_discontinued) is skipped as discontinued")
end
end
context "when a cask without a livecheckable has `version :latest`" do
it "errors" do
expect { skip_conditions.referenced_skip_information(casks[:latest], original_name) }
.to raise_error(RuntimeError, "Referenced cask (test_latest) is skipped as latest")
end
end
context "when a cask without a livecheckable has an unversioned URL" do
it "errors" do
expect { skip_conditions.referenced_skip_information(casks[:unversioned], original_name) }
.to raise_error(RuntimeError, "Referenced cask (test_unversioned) is skipped as unversioned")
end
end
context "when a cask has a `livecheck` block containing `skip`" do
it "skips" do
expect(skip_conditions.referenced_skip_information(casks[:skip], original_name))
.to eq(status_hashes[:cask][:skip].merge({ cask: original_name }))
expect(skip_conditions.referenced_skip_information(casks[:skip_with_message], original_name))
.to eq(status_hashes[:cask][:skip_with_message].merge({ cask: original_name }))
end
end
it "returns an empty hash for a non-skippable formula" do
expect(skip_conditions.referenced_skip_information(formulae[:basic], original_name)).to eq(nil)
end
it "returns an empty hash for a non-skippable cask" do
expect(skip_conditions.referenced_skip_information(casks[:basic], original_name)).to eq(nil)
end
end
describe "::print_skip_information" do
context "when a formula without a livecheckable is deprecated" do
it "prints skip information" do

View File

@ -28,6 +28,40 @@ describe Livecheck do
end
let(:livecheckable_c) { described_class.new(c) }
describe "#formula" do
it "returns nil if not set" do
expect(livecheckable_f.formula).to be nil
end
it "returns the String if set" do
livecheckable_f.formula("other-formula")
expect(livecheckable_f.formula).to eq("other-formula")
end
it "raises a TypeError if the argument isn't a String" do
expect {
livecheckable_f.formula(123)
}.to raise_error(TypeError, "Livecheck#formula expects a String")
end
end
describe "#cask" do
it "returns nil if not set" do
expect(livecheckable_c.cask).to be nil
end
it "returns the String if set" do
livecheckable_c.cask("other-cask")
expect(livecheckable_c.cask).to eq("other-cask")
end
it "raises a TypeError if the argument isn't a String" do
expect {
livecheckable_c.cask(123)
}.to raise_error(TypeError, "Livecheck#cask expects a String")
end
end
describe "#regex" do
it "returns nil if not set" do
expect(livecheckable_f.regex).to be nil
@ -128,6 +162,8 @@ describe Livecheck do
it "returns a Hash of all instance variables" do
expect(livecheckable_f.to_hash).to eq(
{
"cask" => nil,
"formula" => nil,
"regex" => nil,
"skip" => false,
"skip_msg" => nil,