Sparkle: Move sorting/filtering into methods

We need to be able to replicate the `Sparkle` strategy's sorting
and filtering behavior in a related cask audit, so this extracts
the logic into reusable methods.

This also stores `item.minimum_system_version` as a `MacOSVersion`
object (instead of a string), so we can do proper version comparison
(instead of naive string comparison) wherever needed.
This commit is contained in:
Sam Ford 2023-11-08 09:10:09 -05:00
parent 4f469234d0
commit bc2ce97e5d
No known key found for this signature in database
GPG Key ID: 7AF5CBEE1DD6F76D
2 changed files with 113 additions and 17 deletions

View File

@ -100,6 +100,16 @@ module Homebrew
short_version ||= item.elements["shortVersionString"]&.text&.strip&.presence
version ||= item.elements["version"]&.text&.strip&.presence
minimum_system_version_text =
item.elements["minimumSystemVersion"]&.text&.strip&.gsub(/\A\D+|\D+\z/, "")&.presence
if minimum_system_version_text.present?
minimum_system_version = begin
MacOSVersion.new(minimum_system_version_text)
rescue MacOSVersion::Error
nil
end
end
pub_date = item.elements["pubDate"]&.text&.strip&.presence&.then do |date_string|
Time.parse(date_string)
rescue ArgumentError
@ -114,18 +124,6 @@ module Homebrew
bundle_version = BundleVersion.new(short_version, version) if short_version || version
next if os && !((os == "osx") || (os == "macos"))
if (minimum_system_version = item.elements["minimumSystemVersion"]&.text&.gsub(/\A\D+|\D+\z/, ""))
macos_minimum_system_version = begin
MacOSVersion.new(minimum_system_version).strip_patch
rescue MacOSVersion::Error
nil
end
next if macos_minimum_system_version&.prerelease?
end
data = {
title: title,
link: link,
@ -146,6 +144,33 @@ module Homebrew
end.compact
end
# Filters out items that aren't suitable for Homebrew.
#
# @param items [Array] appcast items
# @return [Array]
sig { params(items: T::Array[Item]).returns(T::Array[Item]) }
def self.filter_items(items)
items.select do |item|
# Omit items with an explicit `os` value that isn't macOS
next false if item.os && !((item.os == "osx") || (item.os == "macos"))
# Omit items for prerelease macOS versions
next false if item.minimum_system_version&.strip_patch&.prerelease?
true
end.compact
end
# Sorts items from newest to oldest.
#
# @param items [Array] appcast items
# @return [Array]
sig { params(items: T::Array[Item]).returns(T::Array[Item]) }
def self.sort_items(items)
items.sort_by { |item| [item.pub_date, item.bundle_version] }
.reverse
end
# Uses `#items_from_content` to identify versions from the Sparkle
# appcast content or, if a block is provided, passes the content to
# the block to handle matching.
@ -161,7 +186,7 @@ module Homebrew
).returns(T::Array[String])
}
def self.versions_from_content(content, regex = nil, &block)
items = items_from_content(content).sort_by { |item| [item.pub_date, item.bundle_version] }.reverse
items = sort_items(filter_items(items_from_content(content)))
return [] if items.blank?
item = items.first

View File

@ -28,6 +28,17 @@ describe Homebrew::Livecheck::Strategy::Sparkle do
# `Sparkle::Item` objects.
let(:item_hashes) do
{
# The 1.2.4 version is only used in tests as the basis for an item that
# should be excluded (after modifications).
v124: {
title: "Version 1.2.4",
release_notes_link: "https://www.example.com/example/1.2.4.html",
pub_date: "Fri, 02 Jan 2021 01:23:45 +0000",
url: "https://www.example.com/example/example-1.2.4.tar.gz",
short_version: "1.2.4",
version: "124",
minimum_system_version: "10.10",
},
v123: {
title: "Version 1.2.3",
release_notes_link: "https://www.example.com/example/1.2.3.html",
@ -128,6 +139,16 @@ describe Homebrew::Livecheck::Strategy::Sparkle do
</item>
EOS
# Set the first item in a copy of `appcast` to a bad `minimumSystemVersion`
# value, to test `MacOSVersion::Error` handling.
bad_macos_version = appcast.sub(
v123_item,
v123_item.sub(
/(<sparkle:minimumSystemVersion>)[^<]+?</m,
'\1Not a macOS version<',
),
)
# Set the first item in a copy of `appcast` to the "beta" channel, to test
# filtering items by channel using a `strategy` block.
beta_channel_item = appcast.sub(
@ -155,6 +176,7 @@ describe Homebrew::Livecheck::Strategy::Sparkle do
{
appcast: appcast,
omitted_items: omitted_items,
bad_macos_version: bad_macos_version,
beta_channel_item: beta_channel_item,
no_versions_item: no_versions_item,
no_items: no_items,
@ -166,6 +188,17 @@ describe Homebrew::Livecheck::Strategy::Sparkle do
let(:items) do
{
v124: Homebrew::Livecheck::Strategy::Sparkle::Item.new(
title: item_hashes[:v124][:title],
release_notes_link: item_hashes[:v124][:release_notes_link],
pub_date: Time.parse(item_hashes[:v124][:pub_date]),
url: item_hashes[:v124][:url],
bundle_version: Homebrew::BundleVersion.new(
item_hashes[:v124][:short_version],
item_hashes[:v124][:version],
),
minimum_system_version: MacOSVersion.new(item_hashes[:v124][:minimum_system_version]),
),
v123: Homebrew::Livecheck::Strategy::Sparkle::Item.new(
title: item_hashes[:v123][:title],
release_notes_link: item_hashes[:v123][:release_notes_link],
@ -175,7 +208,7 @@ describe Homebrew::Livecheck::Strategy::Sparkle do
item_hashes[:v123][:short_version],
item_hashes[:v123][:version],
),
minimum_system_version: item_hashes[:v123][:minimum_system_version],
minimum_system_version: MacOSVersion.new(item_hashes[:v123][:minimum_system_version]),
),
v122: Homebrew::Livecheck::Strategy::Sparkle::Item.new(
title: item_hashes[:v122][:title],
@ -189,7 +222,7 @@ describe Homebrew::Livecheck::Strategy::Sparkle do
item_hashes[:v122][:short_version],
item_hashes[:v122][:version],
),
minimum_system_version: item_hashes[:v122][:minimum_system_version],
minimum_system_version: MacOSVersion.new(item_hashes[:v122][:minimum_system_version]),
),
v121: Homebrew::Livecheck::Strategy::Sparkle::Item.new(
title: item_hashes[:v121][:title],
@ -201,7 +234,7 @@ describe Homebrew::Livecheck::Strategy::Sparkle do
item_hashes[:v121][:short_version],
item_hashes[:v121][:version],
),
minimum_system_version: item_hashes[:v121][:minimum_system_version],
minimum_system_version: MacOSVersion.new(item_hashes[:v121][:minimum_system_version]),
),
v120: Homebrew::Livecheck::Strategy::Sparkle::Item.new(
title: item_hashes[:v120][:title],
@ -213,7 +246,7 @@ describe Homebrew::Livecheck::Strategy::Sparkle do
item_hashes[:v120][:short_version],
item_hashes[:v120][:version],
),
minimum_system_version: item_hashes[:v120][:minimum_system_version],
minimum_system_version: MacOSVersion.new(item_hashes[:v120][:minimum_system_version]),
),
}
end
@ -234,6 +267,15 @@ describe Homebrew::Livecheck::Strategy::Sparkle do
],
}
bad_macos_version_item = items[:v123].clone
bad_macos_version_item.minimum_system_version = nil
item_arrays[:bad_macos_version] = [
bad_macos_version_item,
items[:v122],
items[:v121],
items[:v120],
]
beta_channel_item = items[:v123].clone
beta_channel_item.channel = "beta"
item_arrays[:beta_channel_item] = [
@ -278,11 +320,40 @@ describe Homebrew::Livecheck::Strategy::Sparkle do
expect(items_from_appcast[0].short_version).to eq(item_hashes[:v123][:short_version])
expect(items_from_appcast[0].version).to eq(item_hashes[:v123][:version])
expect(sparkle.items_from_content(xml[:bad_macos_version])).to eq(item_arrays[:bad_macos_version])
expect(sparkle.items_from_content(xml[:beta_channel_item])).to eq(item_arrays[:beta_channel_item])
expect(sparkle.items_from_content(xml[:no_versions_item])).to eq(item_arrays[:no_versions_item])
end
end
describe "::filter_items" do
let(:items_non_mac_os) do
item = items[:v124].clone
item.os = "not-osx-or-macos"
item_arrays[:appcast] + [item]
end
let(:items_prerelease_minimum_system_version) do
item = items[:v124].clone
item.minimum_system_version = MacOSVersion.new("100")
item_arrays[:appcast] + [item]
end
it "removes items with a non-mac OS" do
expect(sparkle.filter_items(items_non_mac_os)).to eq(item_arrays[:appcast])
end
it "removes items with a prerelease minimumSystemVersion" do
expect(sparkle.filter_items(items_prerelease_minimum_system_version)).to eq(item_arrays[:appcast])
end
end
describe "::sort_items" do
it "returns a sorted array of items" do
expect(sparkle.sort_items(item_arrays[:appcast])).to eq(item_arrays[:appcast_sorted])
end
end
# `#versions_from_content` sorts items by `pub_date` and `bundle_version`, so
# these tests have to account for this behavior in the expected output.
# For example, the version 122 item doesn't have a parseable `pub_date` and