Standardize valid strategy block return types
Valid `strategy` block return types currently vary between strategies. Some only accept a string whereas others accept a string or array of strings. [`strategy` blocks also accept a `nil` return (to simplify early returns) but this was already standardized across strategies.] While some strategies only identify one version by default (where a string is an appropriate return type), it could be that a strategy block identifies more than one version. In this situation, the strategy would need to be modified to accept (and work with) an array from a `strategy` block. Rather than waiting for this to become a problem, this modifies all strategies to standardize on allowing `strategy` blocks to return a string or array of strings (even if only one of these is currently used in practice). Standardizing valid return types helps to further simplify the mental model for `strategy` blocks and reduce cognitive load. This commit extracts related logic from `#find_versions` into methods like `#versions_from_content`, which is conceptually similar to `PageMatch#page_matches` (renamed to `#versions_from_content` for consistency). This allows us to write tests for the related code without having to make network requests (or stub them) at this point. In general, this also helps to better align the structure of strategies and how the various `#find_versions` methods work with versions. There's still more planned work to be done here but this is a step in the right direction.
This commit is contained in:
parent
cf79ced740
commit
56dd89114d
@ -75,6 +75,10 @@ module Homebrew
|
||||
# In rare cases, this can also be a double newline (`\n\n`).
|
||||
HTTP_HEAD_BODY_SEPARATOR = "\r\n\r\n"
|
||||
|
||||
# An error message to use when a `strategy` block returns a value of
|
||||
# an inappropriate type.
|
||||
INVALID_BLOCK_RETURN_VALUE_MSG = "Return value of a strategy block must be a string or array of strings."
|
||||
|
||||
# Creates and/or returns a `@strategies` `Hash`, which maps a snake
|
||||
# case strategy name symbol (e.g. `:page_match`) to the associated
|
||||
# {Strategy}.
|
||||
@ -218,6 +222,25 @@ module Homebrew
|
||||
messages: [error_msg.presence || "cURL failed without an error"],
|
||||
}
|
||||
end
|
||||
|
||||
# Handles the return value from a `strategy` block in a `livecheck`
|
||||
# block.
|
||||
#
|
||||
# @param value [] the return value from a `strategy` block
|
||||
# @return [Array]
|
||||
sig { params(value: T.untyped).returns(T::Array[String]) }
|
||||
def self.handle_block_return(value)
|
||||
case value
|
||||
when String
|
||||
[value]
|
||||
when Array
|
||||
value.compact.uniq
|
||||
when nil
|
||||
[]
|
||||
else
|
||||
raise TypeError, INVALID_BLOCK_RETURN_VALUE_MSG
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -30,34 +30,28 @@ module Homebrew
|
||||
URL_MATCH_REGEX.match?(url)
|
||||
end
|
||||
|
||||
# Extract version information from page content.
|
||||
# Parses YAML text and identifies versions in it.
|
||||
#
|
||||
# @param content [String] the content to check
|
||||
# @return [String]
|
||||
# @param content [String] the YAML text to parse and check
|
||||
# @return [Array]
|
||||
sig {
|
||||
params(
|
||||
content: String,
|
||||
block: T.nilable(T.proc.params(arg0: T::Hash[String, T.untyped]).returns(T.nilable(String))),
|
||||
).returns(T.nilable(String))
|
||||
block: T.nilable(
|
||||
T.proc.params(arg0: T::Hash[String, T.untyped]).returns(T.any(String, T::Array[String], NilClass)),
|
||||
),
|
||||
).returns(T::Array[String])
|
||||
}
|
||||
def self.version_from_content(content, &block)
|
||||
def self.versions_from_content(content, &block)
|
||||
require "yaml"
|
||||
|
||||
yaml = YAML.safe_load(content)
|
||||
return if yaml.blank?
|
||||
return [] if yaml.blank?
|
||||
|
||||
if block
|
||||
case (value = block.call(yaml))
|
||||
when String
|
||||
return value
|
||||
when nil
|
||||
return
|
||||
else
|
||||
raise TypeError, "Return value of `strategy :electron_builder` block must be a string."
|
||||
end
|
||||
end
|
||||
return Strategy.handle_block_return(block.call(yaml)) if block
|
||||
|
||||
yaml["version"]
|
||||
version = yaml["version"]
|
||||
version.present? ? [version] : []
|
||||
end
|
||||
|
||||
# Checks the content at the URL for new versions.
|
||||
@ -81,8 +75,9 @@ module Homebrew
|
||||
match_data.merge!(Strategy.page_content(url))
|
||||
content = match_data.delete(:content)
|
||||
|
||||
version = version_from_content(content, &block)
|
||||
match_data[:matches][version] = Version.new(version) if version
|
||||
versions_from_content(content, &block).each do |version_text|
|
||||
match_data[:matches][version_text] = Version.new(version_text)
|
||||
end
|
||||
|
||||
match_data
|
||||
end
|
||||
|
@ -50,13 +50,37 @@ module Homebrew
|
||||
delegate short_version: :bundle_version
|
||||
end
|
||||
|
||||
# Checks the content at the URL for new versions.
|
||||
# Identify versions from `Item`s produced using
|
||||
# {UnversionedCaskChecker} version information.
|
||||
#
|
||||
# @param items [Hash] a hash of `Item`s containing version information
|
||||
# @return [Array]
|
||||
sig {
|
||||
params(
|
||||
items: T::Hash[String, Item],
|
||||
block: T.nilable(
|
||||
T.proc.params(arg0: T::Hash[String, Item]).returns(T.any(String, T::Array[String], NilClass)),
|
||||
),
|
||||
).returns(T::Array[String])
|
||||
}
|
||||
def self.versions_from_items(items, &block)
|
||||
return Strategy.handle_block_return(block.call(items)) if block
|
||||
|
||||
items.map do |_key, item|
|
||||
item.bundle_version.nice_version
|
||||
end.compact.uniq
|
||||
end
|
||||
|
||||
# Uses {UnversionedCaskChecker} on the provided cask to identify
|
||||
# versions from `plist` files.
|
||||
sig {
|
||||
params(
|
||||
url: String,
|
||||
regex: T.nilable(Regexp),
|
||||
cask: Cask::Cask,
|
||||
block: T.nilable(T.proc.params(arg0: T::Hash[String, Item]).returns(T.nilable(String))),
|
||||
block: T.nilable(
|
||||
T.proc.params(arg0: T::Hash[String, Item]).returns(T.any(String, T::Array[String], NilClass)),
|
||||
),
|
||||
).returns(T::Hash[Symbol, T.untyped])
|
||||
}
|
||||
def self.find_versions(url, regex, cask:, &block)
|
||||
@ -66,22 +90,10 @@ module Homebrew
|
||||
match_data = { matches: {}, regex: regex, url: url }
|
||||
|
||||
unversioned_cask_checker = UnversionedCaskChecker.new(cask)
|
||||
versions = unversioned_cask_checker.all_versions.transform_values { |v| Item.new(bundle_version: v) }
|
||||
items = unversioned_cask_checker.all_versions.transform_values { |v| Item.new(bundle_version: v) }
|
||||
|
||||
if block
|
||||
case (value = block.call(versions))
|
||||
when String
|
||||
match_data[:matches][value] = Version.new(value)
|
||||
when nil
|
||||
return match_data
|
||||
else
|
||||
raise TypeError, "Return value of `strategy :extract_plist` block must be a string."
|
||||
end
|
||||
elsif versions.any?
|
||||
versions.each_value do |item|
|
||||
version = item.bundle_version.nice_version
|
||||
match_data[:matches][version] = Version.new(version)
|
||||
end
|
||||
versions_from_items(items, &block).each do |version_text|
|
||||
match_data[:matches][version_text] = Version.new(version_text)
|
||||
end
|
||||
|
||||
match_data
|
||||
|
@ -30,6 +30,19 @@ module Homebrew
|
||||
# lowest to highest).
|
||||
PRIORITY = 8
|
||||
|
||||
# The default regex used to naively identify numeric versions from tags
|
||||
# when a regex isn't provided.
|
||||
DEFAULT_REGEX = /\D*(.+)/.freeze
|
||||
|
||||
# Whether the strategy can be applied to the provided URL.
|
||||
#
|
||||
# @param url [String] the URL to match against
|
||||
# @return [Boolean]
|
||||
sig { params(url: String).returns(T::Boolean) }
|
||||
def self.match?(url)
|
||||
(DownloadStrategyDetector.detect(url) <= GitDownloadStrategy) == true
|
||||
end
|
||||
|
||||
# Fetches a remote Git repository's tags using `git ls-remote --tags`
|
||||
# and parses the command's output. If a regex is provided, it will be
|
||||
# used to filter out any tags that don't match it.
|
||||
@ -61,12 +74,42 @@ module Homebrew
|
||||
tags_data
|
||||
end
|
||||
|
||||
# Whether the strategy can be applied to the provided URL.
|
||||
# Identify versions from tag strings using a provided regex or the
|
||||
# `DEFAULT_REGEX`. The regex is expected to use a capture group around
|
||||
# the version text.
|
||||
#
|
||||
# @param url [String] the URL to match against
|
||||
# @return [Boolean]
|
||||
def self.match?(url)
|
||||
(DownloadStrategyDetector.detect(url) <= GitDownloadStrategy) == true
|
||||
# @param tags [Array] the tags to identify versions from
|
||||
# @param regex [Regexp, nil] a regex to identify versions
|
||||
# @return [Array]
|
||||
sig {
|
||||
params(
|
||||
tags: T::Array[String],
|
||||
regex: T.nilable(Regexp),
|
||||
block: T.nilable(
|
||||
T.proc.params(arg0: T::Array[String], arg1: T.nilable(Regexp))
|
||||
.returns(T.any(String, T::Array[String], NilClass)),
|
||||
),
|
||||
).returns(T::Array[String])
|
||||
}
|
||||
def self.versions_from_tags(tags, regex = nil, &block)
|
||||
return Strategy.handle_block_return(block.call(tags, regex || DEFAULT_REGEX)) if block
|
||||
|
||||
tags_only_debian = tags.all? { |tag| tag.start_with?("debian/") }
|
||||
|
||||
tags.map do |tag|
|
||||
# Skip tag if it has a 'debian/' prefix and upstream does not do
|
||||
# only 'debian/' prefixed tags
|
||||
next if tag =~ %r{^debian/} && !tags_only_debian
|
||||
|
||||
if regex
|
||||
# Use the first capture group (the version)
|
||||
tag.scan(regex).first&.first
|
||||
else
|
||||
# Remove non-digits from the start of the tag and use that as the
|
||||
# version text
|
||||
tag[DEFAULT_REGEX, 1]
|
||||
end
|
||||
end.compact.uniq
|
||||
end
|
||||
|
||||
# Checks the Git tags for new versions. When a regex isn't provided,
|
||||
@ -82,54 +125,26 @@ module Homebrew
|
||||
regex: T.nilable(Regexp),
|
||||
cask: T.nilable(Cask::Cask),
|
||||
block: T.nilable(
|
||||
T.proc.params(arg0: T::Array[String]).returns(T.any(String, T::Array[String], NilClass)),
|
||||
T.proc.params(arg0: T::Array[String], arg1: T.nilable(Regexp))
|
||||
.returns(T.any(String, T::Array[String], NilClass)),
|
||||
),
|
||||
).returns(T::Hash[Symbol, T.untyped])
|
||||
}
|
||||
def self.find_versions(url, regex, cask: nil, &block)
|
||||
def self.find_versions(url, regex = nil, cask: nil, &block)
|
||||
match_data = { matches: {}, regex: regex, url: url }
|
||||
|
||||
tags_data = tag_info(url, regex)
|
||||
tags = tags_data[:tags]
|
||||
|
||||
if tags_data.key?(:messages)
|
||||
match_data[:messages] = tags_data[:messages]
|
||||
return match_data if tags_data[:tags].blank?
|
||||
return match_data if tags.blank?
|
||||
end
|
||||
|
||||
tags_only_debian = tags_data[:tags].all? { |tag| tag.start_with?("debian/") }
|
||||
|
||||
if block
|
||||
case (value = block.call(tags_data[:tags], regex))
|
||||
when String
|
||||
match_data[:matches][value] = Version.new(value)
|
||||
when Array
|
||||
value.compact.uniq.each do |tag|
|
||||
match_data[:matches][tag] = Version.new(tag)
|
||||
end
|
||||
when nil
|
||||
return match_data
|
||||
else
|
||||
raise TypeError, "Return value of `strategy :git` block must be a string or array of strings."
|
||||
end
|
||||
|
||||
return match_data
|
||||
end
|
||||
|
||||
tags_data[:tags].each do |tag|
|
||||
# Skip tag if it has a 'debian/' prefix and upstream does not do
|
||||
# only 'debian/' prefixed tags
|
||||
next if tag =~ %r{^debian/} && !tags_only_debian
|
||||
|
||||
captures = regex.is_a?(Regexp) ? tag.scan(regex) : []
|
||||
tag_cleaned = if captures[0].is_a?(Array)
|
||||
captures[0][0] # Use the first capture group (the version)
|
||||
else
|
||||
tag[/\D*(.*)/, 1] # Remove non-digits from the start of the tag
|
||||
end
|
||||
|
||||
match_data[:matches][tag] = Version.new(tag_cleaned)
|
||||
versions_from_tags(tags, regex, &block).each do |version_text|
|
||||
match_data[:matches][version_text] = Version.new(version_text)
|
||||
rescue TypeError
|
||||
nil
|
||||
next
|
||||
end
|
||||
|
||||
match_data
|
||||
|
@ -1,4 +1,4 @@
|
||||
# typed: false
|
||||
# typed: true
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "page_match"
|
||||
@ -36,6 +36,39 @@ module Homebrew
|
||||
URL_MATCH_REGEX.match?(url)
|
||||
end
|
||||
|
||||
# Identify versions from HTTP headers.
|
||||
#
|
||||
# @param headers [Hash] a hash of HTTP headers to check for versions
|
||||
# @param regex [Regexp, nil] a regex to use to identify versions
|
||||
# @return [Array]
|
||||
sig {
|
||||
params(
|
||||
headers: T::Hash[String, String],
|
||||
regex: T.nilable(Regexp),
|
||||
block: T.nilable(
|
||||
T.proc.params(
|
||||
arg0: T::Hash[String, String],
|
||||
arg1: T.nilable(Regexp),
|
||||
).returns(T.any(String, T::Array[String], NilClass)),
|
||||
),
|
||||
).returns(T::Array[String])
|
||||
}
|
||||
def self.versions_from_headers(headers, regex = nil, &block)
|
||||
return Strategy.handle_block_return(block.call(headers, regex)) if block
|
||||
|
||||
DEFAULT_HEADERS_TO_CHECK.map do |header_name|
|
||||
header_value = headers[header_name]
|
||||
next if header_value.blank?
|
||||
|
||||
if regex
|
||||
header_value[regex, 1]
|
||||
else
|
||||
v = Version.parse(header_value, detected_from_url: true)
|
||||
v.null? ? nil : v.to_s
|
||||
end
|
||||
end.compact.uniq
|
||||
end
|
||||
|
||||
# Checks the final URL for new versions after following all redirections,
|
||||
# using the provided regex for matching.
|
||||
sig {
|
||||
@ -43,7 +76,9 @@ module Homebrew
|
||||
url: String,
|
||||
regex: T.nilable(Regexp),
|
||||
cask: T.nilable(Cask::Cask),
|
||||
block: T.nilable(T.proc.params(arg0: T::Hash[String, String]).returns(T.nilable(String))),
|
||||
block: T.nilable(
|
||||
T.proc.params(arg0: T::Hash[String, String], arg1: T.nilable(Regexp)).returns(T.nilable(String)),
|
||||
),
|
||||
).returns(T::Hash[Symbol, T.untyped])
|
||||
}
|
||||
def self.find_versions(url, regex, cask: nil, &block)
|
||||
@ -53,36 +88,12 @@ module Homebrew
|
||||
|
||||
# Merge the headers from all responses into one hash
|
||||
merged_headers = headers.reduce(&:merge)
|
||||
return match_data if merged_headers.blank?
|
||||
|
||||
version = if block
|
||||
case (value = block.call(merged_headers, regex))
|
||||
when String
|
||||
value
|
||||
when nil
|
||||
return match_data
|
||||
else
|
||||
raise TypeError, "Return value of `strategy :header_match` block must be a string."
|
||||
end
|
||||
else
|
||||
value = nil
|
||||
DEFAULT_HEADERS_TO_CHECK.each do |header_name|
|
||||
header_value = merged_headers[header_name]
|
||||
next if header_value.blank?
|
||||
|
||||
if regex
|
||||
value = header_value[regex, 1]
|
||||
else
|
||||
v = Version.parse(header_value, detected_from_url: true)
|
||||
value = v.to_s unless v.null?
|
||||
end
|
||||
break if value.present?
|
||||
end
|
||||
|
||||
value
|
||||
versions_from_headers(merged_headers, regex, &block).each do |version_text|
|
||||
match_data[:matches][version_text] = Version.new(version_text)
|
||||
end
|
||||
|
||||
match_data[:matches][version] = Version.new(version) if version
|
||||
|
||||
match_data
|
||||
end
|
||||
end
|
||||
|
@ -54,19 +54,8 @@ module Homebrew
|
||||
),
|
||||
).returns(T::Array[String])
|
||||
}
|
||||
def self.page_matches(content, regex, &block)
|
||||
if block
|
||||
case (value = block.call(content, regex))
|
||||
when String
|
||||
return [value]
|
||||
when Array
|
||||
return value.compact.uniq
|
||||
when nil
|
||||
return []
|
||||
else
|
||||
raise TypeError, "Return value of `strategy :page_match` block must be a string or array of strings."
|
||||
end
|
||||
end
|
||||
def self.versions_from_content(content, regex, &block)
|
||||
return Strategy.handle_block_return(block.call(content, regex)) if block
|
||||
|
||||
content.scan(regex).map do |match|
|
||||
case match
|
||||
@ -109,7 +98,7 @@ module Homebrew
|
||||
end
|
||||
return match_data if content.blank?
|
||||
|
||||
page_matches(content, regex, &block).each do |match_text|
|
||||
versions_from_content(content, regex, &block).each do |match_text|
|
||||
match_data[:matches][match_text] = Version.new(match_text)
|
||||
end
|
||||
|
||||
|
@ -138,6 +138,26 @@ module Homebrew
|
||||
items.max_by { |item| [item.pub_date, item.bundle_version] }
|
||||
end
|
||||
|
||||
# Identify versions from content
|
||||
#
|
||||
# @param content [String] the content to pull version information from
|
||||
# @return [Array]
|
||||
sig {
|
||||
params(
|
||||
content: String,
|
||||
block: T.nilable(T.proc.params(arg0: Item).returns(T.any(String, T::Array[String], NilClass))),
|
||||
).returns(T::Array[String])
|
||||
}
|
||||
def self.versions_from_content(content, &block)
|
||||
item = item_from_content(content)
|
||||
return [] if item.blank?
|
||||
|
||||
return Strategy.handle_block_return(block.call(item)) if block
|
||||
|
||||
version = item.bundle_version&.nice_version
|
||||
version.present? ? [version] : []
|
||||
end
|
||||
|
||||
# Checks the content at the URL for new versions.
|
||||
sig {
|
||||
params(
|
||||
@ -155,21 +175,8 @@ module Homebrew
|
||||
match_data.merge!(Strategy.page_content(url))
|
||||
content = match_data.delete(:content)
|
||||
|
||||
if (item = item_from_content(content))
|
||||
version = if block
|
||||
case (value = block.call(item))
|
||||
when String
|
||||
value
|
||||
when nil
|
||||
return match_data
|
||||
else
|
||||
raise TypeError, "Return value of `strategy :sparkle` block must be a string."
|
||||
end
|
||||
else
|
||||
item.bundle_version&.nice_version
|
||||
end
|
||||
|
||||
match_data[:matches][version] = Version.new(version) if version
|
||||
versions_from_content(content, &block).each do |version_text|
|
||||
match_data[:matches][version_text] = Version.new(version_text)
|
||||
end
|
||||
|
||||
match_data
|
||||
|
@ -1,7 +1,7 @@
|
||||
# typed: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "livecheck/strategy/electron_builder"
|
||||
require "livecheck/strategy"
|
||||
|
||||
describe Homebrew::Livecheck::Strategy::ElectronBuilder do
|
||||
subject(:electron_builder) { described_class }
|
||||
@ -26,6 +26,8 @@ describe Homebrew::Livecheck::Strategy::ElectronBuilder do
|
||||
EOS
|
||||
}
|
||||
|
||||
let(:versions) { ["1.2.3"] }
|
||||
|
||||
describe "::match?" do
|
||||
it "returns true for any URL pointing to a YAML file" do
|
||||
expect(electron_builder.match?(valid_url)).to be true
|
||||
@ -36,32 +38,34 @@ describe Homebrew::Livecheck::Strategy::ElectronBuilder do
|
||||
end
|
||||
end
|
||||
|
||||
describe "::version_from_content" do
|
||||
let(:version_from_electron_builder_yaml) { electron_builder.version_from_content(electron_builder_yaml) }
|
||||
|
||||
it "returns nil if content is blank" do
|
||||
expect(electron_builder.version_from_content("")).to be nil
|
||||
describe "::versions_from_content" do
|
||||
it "returns an empty array if content is blank" do
|
||||
expect(electron_builder.versions_from_content("")).to eq([])
|
||||
end
|
||||
|
||||
it "returns a version string when given YAML data" do
|
||||
expect(version_from_electron_builder_yaml).to be_a(String)
|
||||
it "returns an array of version strings when given YAML text" do
|
||||
expect(electron_builder.versions_from_content(electron_builder_yaml)).to eq(versions)
|
||||
end
|
||||
|
||||
it "returns a version string when given YAML data and a block" do
|
||||
version = electron_builder.version_from_content(electron_builder_yaml) do |yaml|
|
||||
yaml["version"].sub("3", "4")
|
||||
end
|
||||
it "returns an array of version strings when given YAML text and a block" do
|
||||
# Returning a string from block
|
||||
expect(
|
||||
electron_builder.versions_from_content(electron_builder_yaml) do |yaml|
|
||||
yaml["version"].sub("3", "4")
|
||||
end,
|
||||
).to eq(["1.2.4"])
|
||||
|
||||
expect(version).to eq "1.2.4"
|
||||
# Returning an array of strings from block
|
||||
expect(electron_builder.versions_from_content(electron_builder_yaml) { versions }).to eq(versions)
|
||||
end
|
||||
|
||||
it "allows a nil return from a strategy block" do
|
||||
expect(electron_builder.version_from_content(electron_builder_yaml) { next }).to eq(nil)
|
||||
it "allows a nil return from a block" do
|
||||
expect(electron_builder.versions_from_content(electron_builder_yaml) { next }).to eq([])
|
||||
end
|
||||
|
||||
it "errors on an invalid return type from a strategy block" do
|
||||
expect { electron_builder.version_from_content(electron_builder_yaml) { 123 } }
|
||||
.to raise_error(TypeError, "Return value of `strategy :electron_builder` block must be a string.")
|
||||
it "errors on an invalid return type from a block" do
|
||||
expect { electron_builder.versions_from_content(electron_builder_yaml) { 123 } }
|
||||
.to raise_error(TypeError, Homebrew::Livecheck::Strategy::INVALID_BLOCK_RETURN_VALUE_MSG)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -0,0 +1,72 @@
|
||||
# typed: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "livecheck/strategy"
|
||||
require "bundle_version"
|
||||
|
||||
describe Homebrew::Livecheck::Strategy::ExtractPlist do
|
||||
subject(:extract_plist) { described_class }
|
||||
|
||||
let(:http_url) { "https://brew.sh/blog/" }
|
||||
let(:non_http_url) { "ftp://brew.sh/" }
|
||||
|
||||
let(:items) do
|
||||
{
|
||||
"first" => extract_plist::Item.new(
|
||||
bundle_version: Homebrew::BundleVersion.new(nil, "1.2"),
|
||||
),
|
||||
"second" => extract_plist::Item.new(
|
||||
bundle_version: Homebrew::BundleVersion.new(nil, "1.2.3"),
|
||||
),
|
||||
}
|
||||
end
|
||||
|
||||
let(:versions) { ["1.2", "1.2.3"] }
|
||||
|
||||
describe "::match?" do
|
||||
it "returns true for an HTTP URL" do
|
||||
expect(extract_plist.match?(http_url)).to be true
|
||||
end
|
||||
|
||||
it "returns false for a non-HTTP URL" do
|
||||
expect(extract_plist.match?(non_http_url)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe "::versions_from_items" do
|
||||
it "returns an empty array if Items hash is empty" do
|
||||
expect(extract_plist.versions_from_items({})).to eq([])
|
||||
end
|
||||
|
||||
it "returns an array of version strings when given Items" do
|
||||
expect(extract_plist.versions_from_items(items)).to eq(versions)
|
||||
end
|
||||
|
||||
it "returns an array of version strings when given Items and a block" do
|
||||
# Returning a string from block
|
||||
expect(
|
||||
extract_plist.versions_from_items(items) do |items|
|
||||
items["first"].version
|
||||
end,
|
||||
).to eq(["1.2"])
|
||||
|
||||
# Returning an array of strings from block
|
||||
expect(
|
||||
extract_plist.versions_from_items(items) do |items|
|
||||
items.map do |_key, item|
|
||||
item.bundle_version.nice_version
|
||||
end
|
||||
end,
|
||||
).to eq(versions)
|
||||
end
|
||||
|
||||
it "allows a nil return from a block" do
|
||||
expect(extract_plist.versions_from_items(items) { next }).to eq([])
|
||||
end
|
||||
|
||||
it "errors on an invalid return type from a block" do
|
||||
expect { extract_plist.versions_from_items(items) { 123 } }
|
||||
.to raise_error(TypeError, Homebrew::Livecheck::Strategy::INVALID_BLOCK_RETURN_VALUE_MSG)
|
||||
end
|
||||
end
|
||||
end
|
@ -1,7 +1,7 @@
|
||||
# typed: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "livecheck/strategy/git"
|
||||
require "livecheck/strategy"
|
||||
|
||||
describe Homebrew::Livecheck::Strategy::Git do
|
||||
subject(:git) { described_class }
|
||||
@ -9,10 +9,32 @@ describe Homebrew::Livecheck::Strategy::Git do
|
||||
let(:git_url) { "https://github.com/Homebrew/brew.git" }
|
||||
let(:non_git_url) { "https://brew.sh/test" }
|
||||
|
||||
let(:tags) {
|
||||
{
|
||||
normal: ["brew/1.2", "brew/1.2.1", "brew/1.2.2", "brew/1.2.3", "brew/1.2.4", "1.2.5"],
|
||||
hyphens: ["brew/1-2", "brew/1-2-1", "brew/1-2-2", "brew/1-2-3", "brew/1-2-4", "1-2-5"],
|
||||
}
|
||||
}
|
||||
|
||||
let(:regexes) {
|
||||
{
|
||||
standard: /^v?(\d+(?:\.\d+)+)$/i,
|
||||
hyphens: /^v?(\d+(?:[.-]\d+)+)$/i,
|
||||
brew: %r{^brew/v?(\d+(?:\.\d+)+)$}i,
|
||||
}
|
||||
}
|
||||
|
||||
let(:versions) {
|
||||
{
|
||||
default: ["1.2", "1.2.1", "1.2.2", "1.2.3", "1.2.4", "1.2.5"],
|
||||
standard_regex: ["1.2.5"],
|
||||
brew_regex: ["1.2", "1.2.1", "1.2.2", "1.2.3", "1.2.4"],
|
||||
}
|
||||
}
|
||||
|
||||
describe "::tag_info", :needs_network do
|
||||
it "returns the Git tags for the provided remote URL that match the regex provided" do
|
||||
expect(git.tag_info(git_url, /^v?(\d+(?:\.\d+))$/))
|
||||
.not_to be_empty
|
||||
expect(git.tag_info(git_url, regexes[:standard])).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
@ -25,4 +47,46 @@ describe Homebrew::Livecheck::Strategy::Git do
|
||||
expect(git.match?(non_git_url)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe "::versions_from_tags" do
|
||||
it "returns an empty array if tags array is empty" do
|
||||
expect(git.versions_from_tags([])).to eq([])
|
||||
end
|
||||
|
||||
it "returns an array of version strings when given tags" do
|
||||
expect(git.versions_from_tags(tags[:normal])).to eq(versions[:default])
|
||||
expect(git.versions_from_tags(tags[:normal], regexes[:standard])).to eq(versions[:standard_regex])
|
||||
expect(git.versions_from_tags(tags[:normal], regexes[:brew])).to eq(versions[:brew_regex])
|
||||
end
|
||||
|
||||
it "returns an array of version strings when given tags and a block" do
|
||||
# Returning a string from block, default strategy regex
|
||||
expect(git.versions_from_tags(tags[:normal]) { versions[:default].first }).to eq([versions[:default].first])
|
||||
|
||||
# Returning an array of strings from block, default strategy regex
|
||||
expect(
|
||||
git.versions_from_tags(tags[:hyphens]) do |tags, regex|
|
||||
tags.map { |tag| tag[regex, 1]&.tr("-", ".") }
|
||||
end,
|
||||
).to eq(versions[:default])
|
||||
|
||||
# Returning an array of strings from block, explicit regex
|
||||
expect(
|
||||
git.versions_from_tags(tags[:hyphens], regexes[:hyphens]) do |tags, regex|
|
||||
tags.map { |tag| tag[regex, 1]&.tr("-", ".") }
|
||||
end,
|
||||
).to eq(versions[:standard_regex])
|
||||
|
||||
expect(git.versions_from_tags(tags[:hyphens]) { "1.2.3" }).to eq(["1.2.3"])
|
||||
end
|
||||
|
||||
it "allows a nil return from a block" do
|
||||
expect(git.versions_from_tags(tags[:normal]) { next }).to eq([])
|
||||
end
|
||||
|
||||
it "errors on an invalid return type from a block" do
|
||||
expect { git.versions_from_tags(tags[:normal]) { 123 } }
|
||||
.to raise_error(TypeError, Homebrew::Livecheck::Strategy::INVALID_BLOCK_RETURN_VALUE_MSG)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,16 +1,111 @@
|
||||
# typed: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "livecheck/strategy/header_match"
|
||||
require "livecheck/strategy"
|
||||
|
||||
describe Homebrew::Livecheck::Strategy::HeaderMatch do
|
||||
subject(:header_match) { described_class }
|
||||
|
||||
let(:url) { "https://www.example.com/" }
|
||||
|
||||
let(:versions) {
|
||||
versions = {
|
||||
content_disposition: ["1.2.3"],
|
||||
location: ["1.2.4"],
|
||||
}
|
||||
versions[:content_disposition_and_location] = versions[:content_disposition] + versions[:location]
|
||||
|
||||
versions
|
||||
}
|
||||
|
||||
let(:headers) {
|
||||
headers = {
|
||||
content_disposition: {
|
||||
"date" => "Fri, 01 Jan 2021 01:23:45 GMT",
|
||||
"content-type" => "application/x-gzip",
|
||||
"content-length" => "120",
|
||||
"content-disposition" => "attachment; filename=brew-#{versions[:content_disposition].first}.tar.gz",
|
||||
},
|
||||
location: {
|
||||
"date" => "Fri, 01 Jan 2021 01:23:45 GMT",
|
||||
"content-type" => "text/html; charset=utf-8",
|
||||
"location" => "https://github.com/Homebrew/brew/releases/tag/#{versions[:location].first}",
|
||||
"content-length" => "117",
|
||||
},
|
||||
}
|
||||
headers[:content_disposition_and_location] = headers[:content_disposition].merge(headers[:location])
|
||||
|
||||
headers
|
||||
}
|
||||
|
||||
let(:regexes) {
|
||||
{
|
||||
archive: /filename=brew[._-]v?(\d+(?:\.\d+)+)\.t/i,
|
||||
latest: %r{.*?/tag/v?(\d+(?:\.\d+)+)$}i,
|
||||
loose: /v?(\d+(?:\.\d+)+)/i,
|
||||
}
|
||||
}
|
||||
|
||||
describe "::match?" do
|
||||
it "returns true for any URL" do
|
||||
expect(header_match.match?(url)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe "::versions_from_headers" do
|
||||
it "returns an empty array if headers hash is empty" do
|
||||
expect(header_match.versions_from_headers({})).to eq([])
|
||||
end
|
||||
|
||||
it "returns an array of version strings when given headers" do
|
||||
expect(header_match.versions_from_headers(headers[:content_disposition])).to eq(versions[:content_disposition])
|
||||
expect(header_match.versions_from_headers(headers[:location])).to eq(versions[:location])
|
||||
expect(header_match.versions_from_headers(headers[:content_disposition_and_location]))
|
||||
.to eq(versions[:content_disposition_and_location])
|
||||
|
||||
expect(header_match.versions_from_headers(headers[:content_disposition], regexes[:archive]))
|
||||
.to eq(versions[:content_disposition])
|
||||
expect(header_match.versions_from_headers(headers[:location], regexes[:latest])).to eq(versions[:location])
|
||||
expect(header_match.versions_from_headers(headers[:content_disposition_and_location], regexes[:latest]))
|
||||
.to eq(versions[:location])
|
||||
end
|
||||
|
||||
it "returns an array of version strings when given headers and a block" do
|
||||
# Returning a string from block, no regex
|
||||
expect(
|
||||
header_match.versions_from_headers(headers[:location]) do |headers|
|
||||
v = Version.parse(headers["location"], detected_from_url: true)
|
||||
v.null? ? nil : v.to_s
|
||||
end,
|
||||
).to eq(versions[:location])
|
||||
|
||||
# Returning a string from block, explicit regex
|
||||
expect(
|
||||
header_match.versions_from_headers(headers[:location], regexes[:latest]) do |headers, regex|
|
||||
headers["location"] ? headers["location"][regex, 1] : nil
|
||||
end,
|
||||
).to eq(versions[:location])
|
||||
|
||||
# Returning an array of strings from block
|
||||
# NOTE: Strategies runs `#compact` on an array from a block, so nil
|
||||
# values are filtered out without needing to use `#compact` in the block.
|
||||
expect(
|
||||
header_match.versions_from_headers(
|
||||
headers[:content_disposition_and_location],
|
||||
regexes[:loose],
|
||||
) do |headers, regex|
|
||||
headers.transform_values { |header| header[regex, 1] }.values
|
||||
end,
|
||||
).to eq(versions[:content_disposition_and_location])
|
||||
end
|
||||
|
||||
it "allows a nil return from a block" do
|
||||
expect(header_match.versions_from_headers(headers[:location]) { next }).to eq([])
|
||||
end
|
||||
|
||||
it "errors on an invalid return type from a block" do
|
||||
expect { header_match.versions_from_headers(headers) { 123 } }
|
||||
.to raise_error(TypeError, Homebrew::Livecheck::Strategy::INVALID_BLOCK_RETURN_VALUE_MSG)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,7 +1,7 @@
|
||||
# typed: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "livecheck/strategy/page_match"
|
||||
require "livecheck/strategy"
|
||||
|
||||
describe Homebrew::Livecheck::Strategy::PageMatch do
|
||||
subject(:page_match) { described_class }
|
||||
@ -9,7 +9,7 @@ describe Homebrew::Livecheck::Strategy::PageMatch do
|
||||
let(:url) { "https://brew.sh/blog/" }
|
||||
let(:regex) { %r{href=.*?/homebrew[._-]v?(\d+(?:\.\d+)+)/?["' >]}i }
|
||||
|
||||
let(:page_content) {
|
||||
let(:content) {
|
||||
<<~EOS
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@ -35,7 +35,7 @@ describe Homebrew::Livecheck::Strategy::PageMatch do
|
||||
EOS
|
||||
}
|
||||
|
||||
let(:page_content_matches) { ["2.6.0", "2.5.0", "2.4.0", "2.3.0", "2.2.0", "2.1.0", "2.0.0", "1.9.0"] }
|
||||
let(:content_matches) { ["2.6.0", "2.5.0", "2.4.0", "2.3.0", "2.2.0", "2.1.0", "2.0.0", "1.9.0"] }
|
||||
|
||||
let(:find_versions_return_hash) {
|
||||
{
|
||||
@ -66,24 +66,41 @@ describe Homebrew::Livecheck::Strategy::PageMatch do
|
||||
end
|
||||
end
|
||||
|
||||
describe "::page_matches" do
|
||||
it "finds matching text in page content using a regex" do
|
||||
expect(page_match.page_matches(page_content, regex)).to eq(page_content_matches)
|
||||
describe "::versions_from_content" do
|
||||
it "returns an empty array if content is blank" do
|
||||
expect(page_match.versions_from_content("", regex)).to eq([])
|
||||
end
|
||||
|
||||
it "finds matching text in page content using a strategy block" do
|
||||
expect(page_match.page_matches(page_content, regex) { |content, regex| content.scan(regex).map(&:first).uniq })
|
||||
.to eq(page_content_matches)
|
||||
it "returns an array of version strings when given content" do
|
||||
expect(page_match.versions_from_content(content, regex)).to eq(content_matches)
|
||||
|
||||
# Regexes should use a capture group around the version but a regex
|
||||
# without one should still be handled
|
||||
expect(page_match.versions_from_content(content, /\d+(?:\.\d+)+/i)).to eq(content_matches)
|
||||
end
|
||||
|
||||
it "allows a nil return from a strategy block" do
|
||||
expect(page_match.page_matches(page_content, regex) { next }).to eq([])
|
||||
it "returns an array of version strings when given content and a block" do
|
||||
# Returning a string from block
|
||||
expect(page_match.versions_from_content(content, regex) { "1.2.3" }).to eq(["1.2.3"])
|
||||
|
||||
# Returning an array of strings from block
|
||||
expect(page_match.versions_from_content(content, regex) { |page, regex| page.scan(regex).map(&:first) })
|
||||
.to eq(content_matches)
|
||||
end
|
||||
|
||||
it "allows a nil return from a block" do
|
||||
expect(page_match.versions_from_content(content, regex) { next }).to eq([])
|
||||
end
|
||||
|
||||
it "errors on an invalid return type from a block" do
|
||||
expect { page_match.versions_from_content(content, regex) { 123 } }
|
||||
.to raise_error(TypeError, Homebrew::Livecheck::Strategy::INVALID_BLOCK_RETURN_VALUE_MSG)
|
||||
end
|
||||
end
|
||||
|
||||
describe "::find_versions?" do
|
||||
it "finds versions in provided_content" do
|
||||
expect(page_match.find_versions(url, regex, provided_content: page_content))
|
||||
expect(page_match.find_versions(url, regex, provided_content: content))
|
||||
.to eq(find_versions_cached_return_hash)
|
||||
end
|
||||
end
|
||||
|
@ -1,7 +1,8 @@
|
||||
# typed: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "livecheck/strategy/sparkle"
|
||||
require "livecheck/strategy"
|
||||
require "bundle_version"
|
||||
|
||||
describe Homebrew::Livecheck::Strategy::Sparkle do
|
||||
subject(:sparkle) { described_class }
|
||||
@ -11,6 +12,7 @@ describe Homebrew::Livecheck::Strategy::Sparkle do
|
||||
let(:appcast_data) {
|
||||
{
|
||||
title: "Version 1.2.3",
|
||||
pub_date: "Fri, 01 Jan 2021 01:23:45 +0000",
|
||||
url: "https://www.example.com/example/example.tar.gz",
|
||||
short_version: "1.2.3",
|
||||
version: "1234",
|
||||
@ -30,6 +32,7 @@ describe Homebrew::Livecheck::Strategy::Sparkle do
|
||||
<title>#{appcast_data[:title]}</title>
|
||||
<sparkle:minimumSystemVersion>10.10</sparkle:minimumSystemVersion>
|
||||
<sparkle:releaseNotesLink>https://www.example.com/example/1.2.3.html</sparkle:releaseNotesLink>
|
||||
<pubDate>#{appcast_data[:pub_date]}</pubDate>
|
||||
<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>
|
||||
@ -37,6 +40,17 @@ describe Homebrew::Livecheck::Strategy::Sparkle do
|
||||
EOS
|
||||
}
|
||||
|
||||
let(:item) {
|
||||
Homebrew::Livecheck::Strategy::Sparkle::Item.new(
|
||||
title: appcast_data[:title],
|
||||
pub_date: Time.parse(appcast_data[:pub_date]),
|
||||
url: appcast_data[:url],
|
||||
bundle_version: Homebrew::BundleVersion.new(appcast_data[:short_version], appcast_data[:version]),
|
||||
)
|
||||
}
|
||||
|
||||
let(:versions) { [item.bundle_version.nice_version] }
|
||||
|
||||
describe "::match?" do
|
||||
it "returns true for any URL" do
|
||||
expect(sparkle.match?(url)).to be true
|
||||
@ -52,10 +66,39 @@ describe Homebrew::Livecheck::Strategy::Sparkle do
|
||||
|
||||
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).to eq(item)
|
||||
expect(item_from_appcast_xml.title).to eq(appcast_data[:title])
|
||||
expect(item_from_appcast_xml.pub_date).to eq(Time.parse(appcast_data[:pub_date]))
|
||||
expect(item_from_appcast_xml.url).to eq(appcast_data[:url])
|
||||
expect(item_from_appcast_xml.short_version).to eq(appcast_data[:short_version])
|
||||
expect(item_from_appcast_xml.version).to eq(appcast_data[:version])
|
||||
end
|
||||
end
|
||||
|
||||
describe "::versions_from_content" do
|
||||
it "returns an array of version strings when given content" do
|
||||
expect(sparkle.versions_from_content(appcast_xml)).to eq(versions)
|
||||
end
|
||||
|
||||
it "returns an array of version strings when given content and a block" do
|
||||
# Returning a string from block
|
||||
expect(
|
||||
sparkle.versions_from_content(appcast_xml) do |item|
|
||||
item.bundle_version&.nice_version&.sub("3", "4")
|
||||
end,
|
||||
).to eq([item.bundle_version.nice_version.sub("3", "4")])
|
||||
|
||||
# Returning an array of strings from block
|
||||
expect(sparkle.versions_from_content(appcast_xml) { versions }).to eq(versions)
|
||||
end
|
||||
|
||||
it "allows a nil return from a block" do
|
||||
expect(sparkle.versions_from_content(appcast_xml) { next }).to eq([])
|
||||
end
|
||||
|
||||
it "errors on an invalid return type from a block" do
|
||||
expect { sparkle.versions_from_content(appcast_xml) { 123 } }
|
||||
.to raise_error(TypeError, Homebrew::Livecheck::Strategy::INVALID_BLOCK_RETURN_VALUE_MSG)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -30,4 +30,20 @@ describe Homebrew::Livecheck::Strategy do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "::handle_block_return" do
|
||||
it "returns an array of version strings when given a valid value" do
|
||||
expect(strategy.handle_block_return("1.2.3")).to eq(["1.2.3"])
|
||||
expect(strategy.handle_block_return(["1.2.3", "1.2.4"])).to eq(["1.2.3", "1.2.4"])
|
||||
end
|
||||
|
||||
it "returns an empty array when given a nil value" do
|
||||
expect(strategy.handle_block_return(nil)).to eq([])
|
||||
end
|
||||
|
||||
it "errors when given an invalid value" do
|
||||
expect { strategy.handle_block_return(123) }
|
||||
.to raise_error(TypeError, strategy::INVALID_BLOCK_RETURN_VALUE_MSG)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
x
Reference in New Issue
Block a user