Merge pull request #9529 from reitermarkus/livecheck-cask-strategies
Add more livecheck strategies for casks.
This commit is contained in:
commit
29e310c3f8
@ -59,7 +59,7 @@ module Homebrew
|
||||
[other.version, other.short_version].map { |v| v&.yield_self(&Version.public_method(:new)) }
|
||||
end
|
||||
|
||||
# Create a nicely formatted version (on a best effor basis).
|
||||
# Create a nicely formatted version (on a best effort basis).
|
||||
sig { returns(String) }
|
||||
def nice_version
|
||||
nice_parts.join(",")
|
||||
|
@ -298,7 +298,7 @@ module Cask
|
||||
end
|
||||
|
||||
def check_hosting_with_appcast
|
||||
return if cask.appcast
|
||||
return if cask.appcast || cask.livecheckable?
|
||||
|
||||
add_appcast = "please add an appcast. See https://github.com/Homebrew/homebrew-cask/blob/HEAD/doc/cask_language_reference/stanzas/appcast.md"
|
||||
|
||||
|
@ -8,6 +8,8 @@
|
||||
# This information is used by the `brew livecheck` command to control its
|
||||
# behavior.
|
||||
class Livecheck
|
||||
extend Forwardable
|
||||
|
||||
# A very brief description of why the formula/cask is skipped (e.g. `No longer
|
||||
# developed or maintained`).
|
||||
# @return [String, nil]
|
||||
@ -67,7 +69,9 @@ class Livecheck
|
||||
#
|
||||
# @param symbol [Symbol] symbol for the desired strategy
|
||||
# @return [Symbol, nil]
|
||||
def strategy(symbol = nil)
|
||||
def strategy(symbol = nil, &block)
|
||||
@strategy_block = block if block
|
||||
|
||||
case symbol
|
||||
when nil
|
||||
@strategy
|
||||
@ -78,6 +82,8 @@ class Livecheck
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :strategy_block
|
||||
|
||||
# Sets the `@url` instance variable to the provided argument or returns the
|
||||
# `@url` instance variable when no argument is provided. The argument can be
|
||||
# a `String` (a URL) or a supported `Symbol` corresponding to a URL in the
|
||||
@ -103,6 +109,9 @@ class Livecheck
|
||||
end
|
||||
end
|
||||
|
||||
delegate version: :@formula_or_cask
|
||||
private :version
|
||||
|
||||
# Returns a `Hash` of all instance variable values.
|
||||
# @return [Hash]
|
||||
def to_hash
|
||||
|
12
Library/Homebrew/livecheck/error.rb
Normal file
12
Library/Homebrew/livecheck/error.rb
Normal file
@ -0,0 +1,12 @@
|
||||
# typed: true
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Homebrew
|
||||
module Livecheck
|
||||
# Error during a livecheck run.
|
||||
#
|
||||
# @api private
|
||||
class Error < RuntimeError
|
||||
end
|
||||
end
|
||||
end
|
@ -1,6 +1,7 @@
|
||||
# typed: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "livecheck/error"
|
||||
require "livecheck/strategy"
|
||||
require "ruby-progressbar"
|
||||
require "uri"
|
||||
@ -30,6 +31,7 @@ module Homebrew
|
||||
STRATEGY_SYMBOLS_TO_SKIP_PREPROCESS_URL = [
|
||||
:github_latest,
|
||||
:page_match,
|
||||
:sparkle,
|
||||
].freeze
|
||||
|
||||
UNSTABLE_VERSION_KEYWORDS = %w[
|
||||
@ -144,7 +146,7 @@ module Homebrew
|
||||
|
||||
if latest.blank?
|
||||
no_versions_msg = "Unable to get versions"
|
||||
raise TypeError, no_versions_msg unless json
|
||||
raise Livecheck::Error, no_versions_msg unless json
|
||||
|
||||
next version_info if version_info.is_a?(Hash) && version_info[:status] && version_info[:messages]
|
||||
|
||||
@ -200,6 +202,7 @@ module Homebrew
|
||||
status_hash(formula_or_cask, "error", [e.to_s], full_name: full_name, verbose: verbose)
|
||||
elsif !quiet
|
||||
onoe "#{Tty.blue}#{formula_or_cask_name(formula_or_cask, full_name: full_name)}#{Tty.reset}: #{e}"
|
||||
$stderr.puts e.backtrace if debug && !e.is_a?(Livecheck::Error)
|
||||
nil
|
||||
end
|
||||
end
|
||||
@ -268,6 +271,7 @@ module Homebrew
|
||||
# @return [Hash, nil, Boolean]
|
||||
def skip_conditions(formula_or_cask, json: false, full_name: false, quiet: false, verbose: false)
|
||||
formula = formula_or_cask if formula_or_cask.is_a?(Formula)
|
||||
cask = formula_or_cask if formula_or_cask.is_a?(Cask::Cask)
|
||||
|
||||
if formula&.deprecated? && !formula.livecheckable?
|
||||
return status_hash(formula, "deprecated", full_name: full_name, verbose: verbose) if json
|
||||
@ -276,6 +280,13 @@ module Homebrew
|
||||
return
|
||||
end
|
||||
|
||||
if cask&.discontinued? && !cask.livecheckable?
|
||||
return status_hash(cask, "discontinued", full_name: full_name, verbose: verbose) if json
|
||||
|
||||
puts "#{Tty.red}#{cask_name(cask, full_name: full_name)}#{Tty.reset} : discontinued" unless quiet
|
||||
return
|
||||
end
|
||||
|
||||
if formula&.disabled? && !formula.livecheckable?
|
||||
return status_hash(formula, "disabled", full_name: full_name, verbose: verbose) if json
|
||||
|
||||
@ -290,6 +301,20 @@ module Homebrew
|
||||
return
|
||||
end
|
||||
|
||||
if cask&.version&.latest? && !cask.livecheckable?
|
||||
return status_hash(cask, "latest", full_name: full_name, verbose: verbose) if json
|
||||
|
||||
puts "#{Tty.red}#{cask_name(cask, full_name: full_name)}#{Tty.reset} : latest" unless quiet
|
||||
return
|
||||
end
|
||||
|
||||
if cask&.url&.unversioned? && !cask.livecheckable?
|
||||
return status_hash(cask, "unversioned", full_name: full_name, verbose: verbose) if json
|
||||
|
||||
puts "#{Tty.red}#{cask_name(cask, full_name: full_name)}#{Tty.reset} : unversioned" unless quiet
|
||||
return
|
||||
end
|
||||
|
||||
if formula&.head_only? && !formula.any_version_installed?
|
||||
head_only_msg = "HEAD only formula must be installed to be livecheckable"
|
||||
return status_hash(formula, "error", [head_only_msg], full_name: full_name, verbose: verbose) if json
|
||||
@ -412,9 +437,9 @@ module Homebrew
|
||||
|
||||
has_livecheckable = formula_or_cask.livecheckable?
|
||||
livecheck = formula_or_cask.livecheck
|
||||
livecheck_url = livecheck.url
|
||||
livecheck_regex = livecheck.regex
|
||||
livecheck_strategy = livecheck.strategy
|
||||
livecheck_url = livecheck.url
|
||||
|
||||
urls = [livecheck_url] if livecheck_url.present?
|
||||
urls ||= checkable_urls(formula_or_cask)
|
||||
@ -452,7 +477,9 @@ module Homebrew
|
||||
strategies = Strategy.from_url(
|
||||
url,
|
||||
livecheck_strategy: livecheck_strategy,
|
||||
url_provided: livecheck_url.present?,
|
||||
regex_provided: livecheck_regex.present?,
|
||||
block_provided: livecheck.strategy_block.present?,
|
||||
)
|
||||
strategy = Strategy.from_symbol(livecheck_strategy)
|
||||
strategy ||= strategies.first
|
||||
@ -467,8 +494,13 @@ module Homebrew
|
||||
puts "Regex: #{livecheck_regex.inspect}" if livecheck_regex.present?
|
||||
end
|
||||
|
||||
if livecheck_strategy == :page_match && livecheck_regex.blank?
|
||||
odebug "#{strategy_name} strategy requires a regex"
|
||||
if livecheck_strategy == :page_match && (livecheck_regex.blank? && livecheck.strategy_block.blank?)
|
||||
odebug "#{strategy_name} strategy requires a regex or block"
|
||||
next
|
||||
end
|
||||
|
||||
if livecheck_strategy.present? && livecheck_url.blank?
|
||||
odebug "#{strategy_name} strategy requires a url"
|
||||
next
|
||||
end
|
||||
|
||||
@ -479,7 +511,7 @@ module Homebrew
|
||||
|
||||
next if strategy.blank?
|
||||
|
||||
strategy_data = strategy.find_versions(url, livecheck_regex)
|
||||
strategy_data = strategy.find_versions(url, livecheck_regex, &livecheck.strategy_block)
|
||||
match_version_map = strategy_data[:matches]
|
||||
regex = strategy_data[:regex]
|
||||
|
||||
|
@ -57,12 +57,12 @@ module Homebrew
|
||||
# @param regex_provided [Boolean] whether a regex is provided in the
|
||||
# `livecheck` block
|
||||
# @return [Array]
|
||||
def from_url(url, livecheck_strategy: nil, regex_provided: nil)
|
||||
def from_url(url, livecheck_strategy: nil, url_provided: nil, regex_provided: nil, block_provided: nil)
|
||||
usable_strategies = strategies.values.select do |strategy|
|
||||
if strategy == PageMatch
|
||||
# Only treat the `PageMatch` strategy as usable if a regex is
|
||||
# present in the `livecheck` block
|
||||
next unless regex_provided
|
||||
next unless regex_provided || block_provided
|
||||
elsif strategy.const_defined?(:PRIORITY) &&
|
||||
!strategy::PRIORITY.positive? &&
|
||||
from_symbol(livecheck_strategy) != strategy
|
||||
@ -80,6 +80,49 @@ module Homebrew
|
||||
(strategy.const_defined?(:PRIORITY) ? -strategy::PRIORITY : -DEFAULT_PRIORITY)
|
||||
end
|
||||
end
|
||||
|
||||
def self.page_headers(url)
|
||||
@headers ||= {}
|
||||
|
||||
return @headers[url] if @headers.key?(url)
|
||||
|
||||
headers = []
|
||||
|
||||
[:default, :browser].each do |user_agent|
|
||||
args = [
|
||||
"--head", # Only work with the response headers
|
||||
"--request", "GET", # Use a GET request (instead of HEAD)
|
||||
"--silent", # Silent mode
|
||||
"--location", # Follow redirects
|
||||
"--connect-timeout", "5", # Max time allowed for connection (secs)
|
||||
"--max-time", "10" # Max time allowed for transfer (secs)
|
||||
]
|
||||
|
||||
stdout, _, status = curl_with_workarounds(
|
||||
*args, url,
|
||||
print_stdout: false, print_stderr: false,
|
||||
debug: false, verbose: false,
|
||||
user_agent: user_agent, retry: false
|
||||
)
|
||||
|
||||
while stdout.match?(/\AHTTP.*\r$/)
|
||||
h, stdout = stdout.split("\r\n\r\n", 2)
|
||||
|
||||
headers << h.split("\r\n").drop(1)
|
||||
.map { |header| header.split(/:\s*/, 2) }
|
||||
.to_h.transform_keys(&:downcase)
|
||||
end
|
||||
|
||||
return (@headers[url] = headers) if status.success?
|
||||
end
|
||||
|
||||
headers
|
||||
end
|
||||
|
||||
def self.page_content(url)
|
||||
@page_content ||= {}
|
||||
@page_content[url] ||= URI.parse(url).open.read
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -92,9 +135,11 @@ require_relative "strategy/github_latest"
|
||||
require_relative "strategy/gnome"
|
||||
require_relative "strategy/gnu"
|
||||
require_relative "strategy/hackage"
|
||||
require_relative "strategy/header_match"
|
||||
require_relative "strategy/launchpad"
|
||||
require_relative "strategy/npm"
|
||||
require_relative "strategy/page_match"
|
||||
require_relative "strategy/pypi"
|
||||
require_relative "strategy/sourceforge"
|
||||
require_relative "strategy/sparkle"
|
||||
require_relative "strategy/xorg"
|
||||
|
78
Library/Homebrew/livecheck/strategy/header_match.rb
Normal file
78
Library/Homebrew/livecheck/strategy/header_match.rb
Normal file
@ -0,0 +1,78 @@
|
||||
# typed: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "page_match"
|
||||
|
||||
module Homebrew
|
||||
module Livecheck
|
||||
module Strategy
|
||||
# The {HeaderMatch} strategy follows all URL redirections and scans
|
||||
# the resulting headers for matching text using the provided regex.
|
||||
#
|
||||
# @api private
|
||||
class HeaderMatch
|
||||
extend T::Sig
|
||||
|
||||
NICE_NAME = "Header match"
|
||||
|
||||
# A priority of zero causes livecheck to skip the strategy. We only
|
||||
# apply {HeaderMatch} using `strategy :header_match` in a `livecheck`
|
||||
# block, as we can't automatically determine when this can be
|
||||
# successfully applied to a URL.
|
||||
PRIORITY = 0
|
||||
|
||||
# The `Regexp` used to determine if the strategy applies to the URL.
|
||||
URL_MATCH_REGEX = %r{^https?://}i.freeze
|
||||
|
||||
# Whether the strategy can be applied to the provided URL.
|
||||
# The strategy will technically match any HTTP URL but is
|
||||
# only usable with a `livecheck` block containing a regex
|
||||
# or block.
|
||||
sig { params(url: String).returns(T::Boolean) }
|
||||
def self.match?(url)
|
||||
URL_MATCH_REGEX.match?(url)
|
||||
end
|
||||
|
||||
# Checks the final URL for new versions after following all redirections,
|
||||
# using the provided regex for matching.
|
||||
sig { params(url: String, regex: T.nilable(Regexp)).returns(T::Hash[Symbol, T.untyped]) }
|
||||
def self.find_versions(url, regex, &block)
|
||||
match_data = { matches: {}, regex: regex, url: url }
|
||||
|
||||
headers = Strategy.page_headers(url)
|
||||
|
||||
# Merge the headers from all responses into one hash
|
||||
merged_headers = headers.reduce(&:merge)
|
||||
|
||||
if block
|
||||
match = block.call(merged_headers)
|
||||
else
|
||||
match = nil
|
||||
|
||||
if (filename = merged_headers["content-disposition"])
|
||||
if regex
|
||||
match ||= filename[regex, 1]
|
||||
else
|
||||
v = Version.parse(filename, detected_from_url: true)
|
||||
match ||= v.to_s unless v.null?
|
||||
end
|
||||
end
|
||||
|
||||
if (location = merged_headers["location"])
|
||||
if regex
|
||||
match ||= location[regex, 1]
|
||||
else
|
||||
v = Version.parse(location, detected_from_url: true)
|
||||
match ||= v.to_s unless v.null?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
match_data[:matches][match] = Version.new(match) if match
|
||||
|
||||
match_data
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -30,8 +30,8 @@ module Homebrew
|
||||
URL_MATCH_REGEX = %r{^https?://}i.freeze
|
||||
|
||||
# Whether the strategy can be applied to the provided URL.
|
||||
# PageMatch will technically match any HTTP URL but it's only usable
|
||||
# when the formula has a `livecheck` block containing a regex.
|
||||
# PageMatch will technically match any HTTP URL but is only
|
||||
# usable with a `livecheck` block containing a regex.
|
||||
#
|
||||
# @param url [String] the URL to match against
|
||||
# @return [Boolean]
|
||||
@ -46,10 +46,28 @@ module Homebrew
|
||||
# @param regex [Regexp] a regex used for matching versions in the
|
||||
# content
|
||||
# @return [Array]
|
||||
def self.page_matches(url, regex)
|
||||
page = URI.parse(url).open.read
|
||||
matches = page.scan(regex)
|
||||
matches.map(&:first).uniq
|
||||
def self.page_matches(url, regex, &block)
|
||||
page = Strategy.page_content(url)
|
||||
|
||||
if block
|
||||
case (value = block.call(page))
|
||||
when String
|
||||
return [value]
|
||||
when Array
|
||||
return value
|
||||
else
|
||||
raise TypeError, "Return value of `strategy :page_match` block must be a string or array of strings."
|
||||
end
|
||||
end
|
||||
|
||||
page.scan(regex).map do |match|
|
||||
case match
|
||||
when String
|
||||
match
|
||||
else
|
||||
match.first
|
||||
end
|
||||
end.uniq
|
||||
end
|
||||
|
||||
# Checks the content at the URL for new versions, using the provided
|
||||
@ -58,10 +76,10 @@ module Homebrew
|
||||
# @param url [String] the URL of the content to check
|
||||
# @param regex [Regexp] a regex used for matching versions in content
|
||||
# @return [Hash]
|
||||
def self.find_versions(url, regex)
|
||||
def self.find_versions(url, regex, &block)
|
||||
match_data = { matches: {}, regex: regex, url: url }
|
||||
|
||||
page_matches(url, regex).each do |match|
|
||||
page_matches(url, regex, &block).each do |match|
|
||||
match_data[:matches][match] = Version.new(match)
|
||||
end
|
||||
|
||||
|
109
Library/Homebrew/livecheck/strategy/sparkle.rb
Normal file
109
Library/Homebrew/livecheck/strategy/sparkle.rb
Normal file
@ -0,0 +1,109 @@
|
||||
# typed: true
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "bundle_version"
|
||||
require_relative "page_match"
|
||||
|
||||
module Homebrew
|
||||
module Livecheck
|
||||
module Strategy
|
||||
# The {Sparkle} strategy fetches content at a URL and parses
|
||||
# it as a Sparkle appcast in XML format.
|
||||
#
|
||||
# @api private
|
||||
class Sparkle
|
||||
extend T::Sig
|
||||
|
||||
# A priority of zero causes livecheck to skip the strategy. We only
|
||||
# apply {Sparkle} using `strategy :sparkle` in a `livecheck` block,
|
||||
# as we can't automatically determine when this can be successfully
|
||||
# applied to a URL without fetching the content.
|
||||
PRIORITY = 0
|
||||
|
||||
# The `Regexp` used to determine if the strategy applies to the URL.
|
||||
URL_MATCH_REGEX = %r{^https?://}i.freeze
|
||||
|
||||
# Whether the strategy can be applied to the provided URL.
|
||||
# The strategy will technically match any HTTP URL but is
|
||||
# only usable with a `livecheck` block containing a regex
|
||||
# or block.
|
||||
sig { params(url: String).returns(T::Boolean) }
|
||||
def self.match?(url)
|
||||
URL_MATCH_REGEX.match?(url)
|
||||
end
|
||||
|
||||
Item = Struct.new(:title, :url, :bundle_version, :short_version, :version, keyword_init: true) do
|
||||
extend T::Sig
|
||||
|
||||
extend Forwardable
|
||||
|
||||
delegate version: :bundle_version
|
||||
delegate short_version: :bundle_version
|
||||
end
|
||||
|
||||
sig { params(content: String).returns(T.nilable(Item)) }
|
||||
def self.item_from_content(content)
|
||||
require "nokogiri"
|
||||
|
||||
xml = Nokogiri::XML(content)
|
||||
xml.remove_namespaces!
|
||||
|
||||
items = xml.xpath("//rss//channel//item").map do |item|
|
||||
enclosure = (item > "enclosure").first
|
||||
|
||||
url = enclosure&.attr("url")
|
||||
short_version = enclosure&.attr("shortVersionString")
|
||||
version = enclosure&.attr("version")
|
||||
|
||||
url ||= (item > "link").first&.text
|
||||
short_version ||= (item > "shortVersionString").first&.text&.strip
|
||||
version ||= (item > "version").first&.text&.strip
|
||||
|
||||
title = (item > "title").first&.text&.strip
|
||||
|
||||
if match = title&.match(/(\d+(?:\.\d+)*)\s*(\([^)]+\))?\Z/)
|
||||
short_version ||= match[1]
|
||||
version ||= match[2]
|
||||
end
|
||||
|
||||
bundle_version = BundleVersion.new(short_version, version) if short_version || version
|
||||
|
||||
data = {
|
||||
title: title,
|
||||
url: url,
|
||||
bundle_version: bundle_version,
|
||||
short_version: bundle_version&.short_version,
|
||||
version: bundle_version&.version,
|
||||
}.compact
|
||||
|
||||
Item.new(**data) unless data.empty?
|
||||
end.compact
|
||||
|
||||
items.max_by(&:bundle_version)
|
||||
end
|
||||
|
||||
# Checks the content at the URL for new versions.
|
||||
sig { params(url: String, regex: T.nilable(Regexp)).returns(T::Hash[Symbol, T.untyped]) }
|
||||
def self.find_versions(url, regex, &block)
|
||||
raise ArgumentError, "The #{T.must(name).demodulize} strategy does not support a regex." if regex
|
||||
|
||||
match_data = { matches: {}, regex: regex, url: url }
|
||||
|
||||
content = Strategy.page_content(url)
|
||||
|
||||
if (item = item_from_content(content))
|
||||
match = if block
|
||||
block.call(item)&.to_s
|
||||
else
|
||||
item.bundle_version&.nice_version
|
||||
end
|
||||
|
||||
match_data[:matches][match] = Version.new(match) if match
|
||||
end
|
||||
|
||||
match_data
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -20,6 +20,24 @@ describe Homebrew::Livecheck do
|
||||
end
|
||||
end
|
||||
|
||||
let(:c) do
|
||||
Cask::CaskLoader.load(+<<-RUBY)
|
||||
cask "test" do
|
||||
version "0.0.1,2"
|
||||
|
||||
url "https://brew.sh/test-0.0.1.tgz"
|
||||
name "Test"
|
||||
desc "Test cask"
|
||||
homepage "https://brew.sh"
|
||||
|
||||
livecheck do
|
||||
url "https://formulae.brew.sh/api/formula/ruby.json"
|
||||
regex(/"stable":"(\d+(?:\.\d+)+)"/i)
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
let(:f_deprecated) do
|
||||
formula("test_deprecated") do
|
||||
desc "Deprecated test formula"
|
||||
@ -29,6 +47,24 @@ describe Homebrew::Livecheck do
|
||||
end
|
||||
end
|
||||
|
||||
let(:c_discontinued) do
|
||||
Cask::CaskLoader.load(+<<-RUBY)
|
||||
cask "test_discontinued" do
|
||||
version "0.0.1"
|
||||
sha256 :no_check
|
||||
|
||||
url "https://brew.sh/test-0.0.1.tgz"
|
||||
name "Test Discontinued"
|
||||
desc "Discontinued test cask"
|
||||
homepage "https://brew.sh"
|
||||
|
||||
caveats do
|
||||
discontinued
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
let(:f_disabled) do
|
||||
formula("test_disabled") do
|
||||
desc "Disabled test formula"
|
||||
@ -38,11 +74,39 @@ describe Homebrew::Livecheck do
|
||||
end
|
||||
end
|
||||
|
||||
let(:f_gist) do
|
||||
formula("test_gist") do
|
||||
desc "Gist test formula"
|
||||
let(:f_versioned) do
|
||||
formula("test@0.0.1") do
|
||||
desc "Versioned test formula"
|
||||
homepage "https://brew.sh"
|
||||
url "https://brew.sh/test-0.0.1.tgz"
|
||||
end
|
||||
end
|
||||
|
||||
let(:c_latest) do
|
||||
Cask::CaskLoader.load(+<<-RUBY)
|
||||
cask "test_latest" do
|
||||
version :latest
|
||||
sha256 :no_check
|
||||
|
||||
url "https://brew.sh/test-0.0.1.tgz"
|
||||
name "Test Latest"
|
||||
desc "Latest test cask"
|
||||
homepage "https://brew.sh"
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
# `URL#unversioned?` doesn't work properly when using the
|
||||
# `Cask::CaskLoader.load` setup above, so we use `Cask::Cask.new` instead.
|
||||
let(:c_unversioned) do
|
||||
Cask::Cask.new "test_unversioned" do
|
||||
version "1.2.3"
|
||||
sha256 :no_check
|
||||
|
||||
url "https://brew.sh/test.tgz"
|
||||
name "Test Unversioned"
|
||||
desc "Unversioned test cask"
|
||||
homepage "https://brew.sh"
|
||||
url "https://gist.github.com/Homebrew/0000000000"
|
||||
end
|
||||
end
|
||||
|
||||
@ -54,6 +118,14 @@ describe Homebrew::Livecheck do
|
||||
end
|
||||
end
|
||||
|
||||
let(:f_gist) do
|
||||
formula("test_gist") do
|
||||
desc "Gist test formula"
|
||||
homepage "https://brew.sh"
|
||||
url "https://gist.github.com/Homebrew/0000000000"
|
||||
end
|
||||
end
|
||||
|
||||
let(:f_skip) do
|
||||
formula("test_skip") do
|
||||
desc "Skipped test formula"
|
||||
@ -66,31 +138,6 @@ describe Homebrew::Livecheck do
|
||||
end
|
||||
end
|
||||
|
||||
let(:f_versioned) do
|
||||
formula("test@0.0.1") do
|
||||
desc "Versioned test formula"
|
||||
homepage "https://brew.sh"
|
||||
url "https://brew.sh/test-0.0.1.tgz"
|
||||
end
|
||||
end
|
||||
|
||||
let(:c) do
|
||||
Cask::CaskLoader.load(+<<-RUBY)
|
||||
cask "test" do
|
||||
version "0.0.1,2"
|
||||
|
||||
url "https://brew.sh/test-0.0.1.tgz"
|
||||
name "Test"
|
||||
homepage "https://brew.sh"
|
||||
|
||||
livecheck do
|
||||
url "https://formulae.brew.sh/api/formula/ruby.json"
|
||||
regex(/"stable":"(\d+(?:\.\d+)+)"/i)
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
describe "::formula_name" do
|
||||
it "returns the name of the formula" do
|
||||
expect(livecheck.formula_name(f)).to eq("test")
|
||||
@ -132,6 +179,12 @@ describe Homebrew::Livecheck do
|
||||
.and not_to_output.to_stderr
|
||||
end
|
||||
|
||||
it "skips a discontinued cask without a livecheckable" do
|
||||
expect { livecheck.skip_conditions(c_discontinued) }
|
||||
.to output("test_discontinued : discontinued\n").to_stdout
|
||||
.and not_to_output.to_stderr
|
||||
end
|
||||
|
||||
it "skips a disabled formula without a livecheckable" do
|
||||
expect { livecheck.skip_conditions(f_disabled) }
|
||||
.to output("test_disabled : disabled\n").to_stdout
|
||||
@ -144,6 +197,18 @@ describe Homebrew::Livecheck do
|
||||
.and not_to_output.to_stderr
|
||||
end
|
||||
|
||||
it "skips a cask containing `version :latest` without a livecheckable" do
|
||||
expect { livecheck.skip_conditions(c_latest) }
|
||||
.to output("test_latest : latest\n").to_stdout
|
||||
.and not_to_output.to_stderr
|
||||
end
|
||||
|
||||
it "skips a cask containing an unversioned URL without a livecheckable" do
|
||||
expect { livecheck.skip_conditions(c_unversioned) }
|
||||
.to output("test_unversioned : unversioned\n").to_stdout
|
||||
.and not_to_output.to_stderr
|
||||
end
|
||||
|
||||
it "skips a HEAD-only formula if not installed" do
|
||||
expect { livecheck.skip_conditions(f_head_only) }
|
||||
.to output("test_head_only : HEAD only formula must be installed to be livecheckable\n").to_stdout
|
||||
|
@ -0,0 +1,16 @@
|
||||
# typed: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "livecheck/strategy/header_match"
|
||||
|
||||
describe Homebrew::Livecheck::Strategy::HeaderMatch do
|
||||
subject(:header_match) { described_class }
|
||||
|
||||
let(:url) { "https://www.example.com/" }
|
||||
|
||||
describe "::match?" do
|
||||
it "returns true for any URL" do
|
||||
expect(header_match.match?(url)).to be true
|
||||
end
|
||||
end
|
||||
end
|
74
Library/Homebrew/test/livecheck/strategy/sparkle_spec.rb
Normal file
74
Library/Homebrew/test/livecheck/strategy/sparkle_spec.rb
Normal file
@ -0,0 +1,74 @@
|
||||
# typed: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "livecheck/strategy/sparkle"
|
||||
|
||||
describe Homebrew::Livecheck::Strategy::Sparkle do
|
||||
subject(:sparkle) { described_class }
|
||||
|
||||
let(:url) { "https://www.example.com/example/appcast.xml" }
|
||||
|
||||
let(:appcast_data) {
|
||||
{
|
||||
title: "Version 1.2.3",
|
||||
url: "https://www.example.com/example/example.tar.gz",
|
||||
bundle_version: Homebrew::BundleVersion.new("1.2.3", "1234"),
|
||||
short_version: "1.2.3",
|
||||
version: "1234",
|
||||
}
|
||||
}
|
||||
|
||||
let(:appcast_item) {
|
||||
Homebrew::Livecheck::Strategy::Sparkle::Item.new(
|
||||
{
|
||||
title: appcast_data[:title],
|
||||
url: appcast_data[:url],
|
||||
bundle_version: appcast_data[:bundle_version],
|
||||
short_version: appcast_data[:bundle_version]&.short_version,
|
||||
version: appcast_data[:bundle_version]&.version,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
let(:appcast_xml) {
|
||||
<<~EOS
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
|
||||
<channel>
|
||||
<title>Example Changelog</title>
|
||||
<link>#{url}</link>
|
||||
<description>Most recent changes with links to updates.</description>
|
||||
<language>en</language>
|
||||
<item>
|
||||
<title>#{appcast_data[:title]}</title>
|
||||
<sparkle:minimumSystemVersion>10.10</sparkle:minimumSystemVersion>
|
||||
<sparkle:releaseNotesLink>https://www.example.com/example/1.2.3.html</sparkle:releaseNotesLink>
|
||||
<enclosure url="#{appcast_data[:url]}" sparkle:shortVersionString="#{appcast_data[:short_version]}" sparkle:version="#{appcast_data[:version]}" length="12345678" type="application/octet-stream" sparkle:dsaSignature="ABCDEF+GHIJKLMNOPQRSTUVWXYZab/cdefghijklmnopqrst/uvwxyz1234567==" />
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
EOS
|
||||
}
|
||||
|
||||
describe "::match?" do
|
||||
it "returns true for any URL" do
|
||||
expect(sparkle.match?(url)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe "::item_from_content" do
|
||||
let(:item_from_appcast_xml) { sparkle.item_from_content(appcast_xml) }
|
||||
|
||||
it "returns nil if content is blank" do
|
||||
expect(sparkle.item_from_content("")).to be nil
|
||||
end
|
||||
|
||||
it "returns an Item when given XML data" do
|
||||
expect(item_from_appcast_xml).to be_a(Homebrew::Livecheck::Strategy::Sparkle::Item)
|
||||
expect(item_from_appcast_xml.title).to eq(appcast_item.title)
|
||||
expect(item_from_appcast_xml.url).to eq(appcast_item.url)
|
||||
expect(item_from_appcast_xml.bundle_version.short_version).to eq(appcast_item.bundle_version.short_version)
|
||||
expect(item_from_appcast_xml.bundle_version.version).to eq(appcast_item.bundle_version.version)
|
||||
end
|
||||
end
|
||||
end
|
@ -318,11 +318,13 @@ class Version
|
||||
end
|
||||
|
||||
def self.parse(spec, detected_from_url: false)
|
||||
version = _parse(spec)
|
||||
version = _parse(spec, detected_from_url: detected_from_url)
|
||||
version.nil? ? NULL : new(version, detected_from_url: detected_from_url)
|
||||
end
|
||||
|
||||
def self._parse(spec)
|
||||
def self._parse(spec, detected_from_url:)
|
||||
spec = CGI.unescape(spec.to_s) if detected_from_url
|
||||
|
||||
spec = Pathname.new(spec) unless spec.is_a? Pathname
|
||||
|
||||
spec_s = spec.to_s
|
||||
@ -465,7 +467,6 @@ class Version
|
||||
m = /[-.vV]?((?:\d+\.)+\d+(?:[-_.]?(?i:alpha|beta|pre|rc)\.?\d{,2})?)/.match(spec_s)
|
||||
return m.captures.first unless m.nil?
|
||||
end
|
||||
|
||||
private_class_method :_parse
|
||||
|
||||
def initialize(val, detected_from_url: false)
|
||||
|
Loading…
x
Reference in New Issue
Block a user