Skip to content

Commit ad28cc7

Browse files
authored
Merge pull request #20108 from Homebrew/macos_version-upgrade-type-sigil
MacOSVersion: enable strong typing, expand tests
2 parents 62ca132 + ea57773 commit ad28cc7

5 files changed

Lines changed: 123 additions & 37 deletions

File tree

Library/Homebrew/ast_constants.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,4 @@
5252
[{ name: :caveats, type: :method_definition }],
5353
[{ name: :plist_options, type: :method_call }, { name: :plist, type: :method_definition }],
5454
[{ name: :test, type: :block_call }],
55-
].freeze, T::Array[[{ name: Symbol, type: Symbol }]])
55+
].freeze, T::Array[T::Array[{ name: Symbol, type: Symbol }]])

Library/Homebrew/cask/dsl.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@ def depends_on(**kwargs)
486486
def add_implicit_macos_dependency
487487
return if (cask_depends_on = @depends_on).present? && cask_depends_on.macos.present?
488488

489-
depends_on macos: ">= :#{MacOSVersion::SYMBOLS.key MacOSVersion::SYMBOLS.values.min}"
489+
depends_on macos: ">= #{MacOSVersion.new(HOMEBREW_MACOS_OLDEST_ALLOWED).to_sym.inspect}"
490490
end
491491

492492
# Declare conflicts that keep a cask from installing or working correctly.

Library/Homebrew/cask/dsl/depends_on.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,17 @@ def macos=(*args)
5252
raise "Only a single 'depends_on macos' is allowed." if defined?(@macos)
5353

5454
# workaround for https://github.com/sorbet/sorbet/issues/6860
55-
first_arg = args.first&.to_s
55+
first_arg = args.first
56+
first_arg_s = first_arg&.to_s
5657

5758
begin
5859
@macos = if args.count > 1
5960
MacOSRequirement.new([args], comparator: "==")
60-
elsif MacOSVersion::SYMBOLS.key?(args.first)
61+
elsif first_arg.is_a?(Symbol) && MacOSVersion::SYMBOLS.key?(first_arg)
6162
MacOSRequirement.new([args.first], comparator: "==")
62-
elsif (md = /^\s*(?<comparator><|>|[=<>]=)\s*:(?<version>\S+)\s*$/.match(first_arg))
63+
elsif (md = /^\s*(?<comparator><|>|[=<>]=)\s*:(?<version>\S+)\s*$/.match(first_arg_s))
6364
MacOSRequirement.new([T.must(md[:version]).to_sym], comparator: md[:comparator])
64-
elsif (md = /^\s*(?<comparator><|>|[=<>]=)\s*(?<version>\S+)\s*$/.match(first_arg))
65+
elsif (md = /^\s*(?<comparator><|>|[=<>]=)\s*(?<version>\S+)\s*$/.match(first_arg_s))
6566
MacOSRequirement.new([md[:version]], comparator: md[:comparator])
6667
# This is not duplicate of the first case: see `args.first` and a different comparator.
6768
else # rubocop:disable Lint/DuplicateBranch

Library/Homebrew/macos_version.rb

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# typed: true # rubocop:todo Sorbet/StrictSigil
1+
# typed: strong
22
# frozen_string_literal: true
33

44
require "version"
@@ -10,6 +10,7 @@ class Error < RuntimeError
1010
sig { returns(T.nilable(T.any(String, Symbol))) }
1111
attr_reader :version
1212

13+
sig { params(version: T.nilable(T.any(String, Symbol))).void }
1314
def initialize(version)
1415
@version = version
1516
super "unknown or unsupported macOS version: #{version.inspect}"
@@ -18,7 +19,7 @@ def initialize(version)
1819

1920
# NOTE: When removing symbols here, ensure that they are added
2021
# to `DEPRECATED_MACOS_VERSIONS` in `MacOSRequirement`.
21-
SYMBOLS = {
22+
SYMBOLS = T.let({
2223
tahoe: "26",
2324
sequoia: "15",
2425
sonoma: "14",
@@ -30,7 +31,7 @@ def initialize(version)
3031
high_sierra: "10.13",
3132
sierra: "10.12",
3233
el_capitan: "10.11",
33-
}.freeze
34+
}.freeze, T::Hash[Symbol, String])
3435

3536
sig { params(macos_version: MacOSVersion).returns(Version) }
3637
def self.kernel_major_version(macos_version)
@@ -57,7 +58,9 @@ def initialize(version)
5758

5859
super(T.must(version))
5960

60-
@comparison_cache = {}
61+
@comparison_cache = T.let({}, T::Hash[T.untyped, T.nilable(Integer)])
62+
@pretty_name = T.let(nil, T.nilable(String))
63+
@sym = T.let(nil, T.nilable(Symbol))
6164
end
6265

6366
sig { override.params(other: T.untyped).returns(T.nilable(Integer)) }
@@ -95,7 +98,7 @@ def strip_patch
9598

9699
sig { returns(Symbol) }
97100
def to_sym
98-
return @sym if defined?(@sym)
101+
return @sym if @sym
99102

100103
sym = SYMBOLS.invert.fetch(strip_patch.to_s, :dunno)
101104

@@ -106,7 +109,7 @@ def to_sym
106109

107110
sig { returns(String) }
108111
def pretty_name
109-
return @pretty_name if defined?(@pretty_name)
112+
return @pretty_name if @pretty_name
110113

111114
pretty_name = to_sym.to_s.split("_").map(&:capitalize).join(" ").freeze
112115

@@ -154,5 +157,7 @@ def requires_nehalem_cpu?
154157
# Represents the absence of a version.
155158
#
156159
# NOTE: Constructor needs to called with an arbitrary macOS-like version which is then set to `nil`.
157-
NULL = MacOSVersion.new("10.0").tap { |v| v.instance_variable_set(:@version, nil) }.freeze
160+
NULL = T.let(MacOSVersion.new("10.0").tap do |v|
161+
T.let(v, MacOSVersion).instance_variable_set(:@version, nil)
162+
end.freeze, MacOSVersion)
158163
end

Library/Homebrew/test/macos_version_spec.rb

Lines changed: 104 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44

55
RSpec.describe MacOSVersion do
66
let(:version) { described_class.new("10.14") }
7+
let(:tahoe_major) { described_class.new("26.0") }
78
let(:big_sur_major) { described_class.new("11.0") }
89
let(:big_sur_update) { described_class.new("11.1") }
10+
let(:frozen_version) { described_class.new("10.14").freeze }
911

10-
describe ".kernel_major_version" do
12+
describe "::kernel_major_version" do
1113
it "returns the kernel major version" do
1214
expect(described_class.kernel_major_version(version)).to eq "18"
15+
expect(described_class.kernel_major_version(tahoe_major)).to eq "25"
1316
expect(described_class.kernel_major_version(big_sur_major)).to eq "20"
1417
expect(described_class.kernel_major_version(big_sur_update)).to eq "20"
1518
end
@@ -19,12 +22,43 @@
1922
end
2023
end
2124

25+
describe "::from_symbol" do
26+
it "raises an error if the symbol is not a valid macOS version" do
27+
expect do
28+
described_class.from_symbol(:foo)
29+
end.to raise_error(MacOSVersion::Error, "unknown or unsupported macOS version: :foo")
30+
end
31+
32+
it "creates a new version from a valid macOS version" do
33+
symbol_version = described_class.from_symbol(:mojave)
34+
expect(symbol_version).to eq(version)
35+
end
36+
end
37+
38+
describe "#new" do
39+
it "raises an error if the version is not a valid macOS version" do
40+
expect do
41+
described_class.new("1.2")
42+
end.to raise_error(MacOSVersion::Error, 'unknown or unsupported macOS version: "1.2"')
43+
end
44+
45+
it "creates a new version from a valid macOS version" do
46+
string_version = described_class.new("11")
47+
expect(string_version).to eq(:big_sur)
48+
end
49+
end
50+
2251
specify "comparison with Symbol" do
2352
expect(version).to be > :high_sierra
2453
expect(version).to eq :mojave
2554
# We're explicitly testing the `===` operator results here.
2655
expect(version).to be === :mojave # rubocop:disable Style/CaseEquality
2756
expect(version).to be < :catalina
57+
58+
# This should work like a normal comparison but the result won't be added
59+
# to the `@comparison_cache` hash because the object is frozen.
60+
expect(frozen_version).to eq :mojave
61+
expect(frozen_version.instance_variable_get(:@comparison_cache)).to eq({})
2862
end
2963

3064
specify "comparison with Integer" do
@@ -64,44 +98,90 @@
6498
end
6599
end
66100

67-
describe "#new" do
68-
it "raises an error if the version is not a valid macOS version" do
69-
expect do
70-
described_class.new("1.2")
71-
end.to raise_error(MacOSVersion::Error, 'unknown or unsupported macOS version: "1.2"')
101+
describe "#strip_patch" do
102+
let(:catalina_update) { described_class.new("10.15.1") }
103+
104+
it "returns the version without the patch" do
105+
expect(big_sur_update.strip_patch).to eq(described_class.new("11"))
106+
expect(catalina_update.strip_patch).to eq(described_class.new("10.15"))
72107
end
73108

74-
it "creates a new version from a valid macOS version" do
75-
string_version = described_class.new("11")
76-
expect(string_version).to eq(:big_sur)
109+
it "returns self if version is null" do
110+
expect(described_class::NULL.strip_patch).to be described_class::NULL
77111
end
78112
end
79113

80-
describe "#from_symbol" do
81-
it "raises an error if the symbol is not a valid macOS version" do
82-
expect do
83-
described_class.from_symbol(:foo)
84-
end.to raise_error(MacOSVersion::Error, "unknown or unsupported macOS version: :foo")
85-
end
114+
specify "#to_sym" do
115+
version_symbol = :mojave
86116

87-
it "creates a new version from a valid macOS version" do
88-
symbol_version = described_class.from_symbol(:mojave)
89-
expect(symbol_version).to eq(version)
90-
end
117+
# We call this more than once to exercise the caching logic
118+
expect(version.to_sym).to eq(version_symbol)
119+
expect(version.to_sym).to eq(version_symbol)
120+
121+
# This should work like a normal but the symbol won't be stored as the
122+
# `@sym` instance variable because the object is frozen.
123+
expect(frozen_version.to_sym).to eq(version_symbol)
124+
expect(frozen_version.instance_variable_get(:@sym)).to be_nil
125+
126+
expect(described_class::NULL.to_sym).to eq(:dunno)
91127
end
92128

93129
specify "#pretty_name" do
130+
version_pretty_name = "Mojave"
131+
94132
expect(described_class.new("10.11").pretty_name).to eq("El Capitan")
95-
expect(described_class.new("10.14").pretty_name).to eq("Mojave")
133+
134+
# We call this more than once to exercise the caching logic
135+
expect(version.pretty_name).to eq(version_pretty_name)
136+
expect(version.pretty_name).to eq(version_pretty_name)
137+
138+
# This should work like a normal but the computed name won't be stored as
139+
# the `@pretty_name` instance variable because the object is frozen.
140+
expect(frozen_version.pretty_name).to eq(version_pretty_name)
141+
expect(frozen_version.instance_variable_get(:@pretty_name)).to be_nil
96142
end
97143

98144
specify "#inspect" do
99145
expect(described_class.new("11").inspect).to eq("#<MacOSVersion: \"11\">")
100146
end
101147

102-
specify "#requires_nehalem_cpu?", :needs_macos do
103-
expect(Hardware::CPU).to receive(:type).at_least(:twice).and_return(:intel)
104-
expect(described_class.new("10.14").requires_nehalem_cpu?).to be true
105-
expect(described_class.new("10.12").requires_nehalem_cpu?).to be false
148+
specify "#outdated_release?" do
149+
expect(described_class.new(described_class::SYMBOLS.values.first).outdated_release?).to be false
150+
expect(described_class.new("10.0").outdated_release?).to be true
151+
end
152+
153+
specify "#prerelease?" do
154+
expect(described_class.new("1000").prerelease?).to be true
155+
end
156+
157+
specify "#unsupported_release?" do
158+
expect(described_class.new("10.0").unsupported_release?).to be true
159+
expect(described_class.new("1000").prerelease?).to be true
160+
end
161+
162+
describe "#requires_nehalem_cpu?", :needs_macos do
163+
context "when CPU is Intel" do
164+
it "returns true if version requires a Nehalem CPU" do
165+
allow(Hardware::CPU).to receive(:type).and_return(:intel)
166+
expect(described_class.new("10.14").requires_nehalem_cpu?).to be true
167+
end
168+
169+
it "returns false if version does not require a Nehalem CPU" do
170+
allow(Hardware::CPU).to receive(:type).and_return(:intel)
171+
expect(described_class.new("10.12").requires_nehalem_cpu?).to be false
172+
end
173+
end
174+
175+
context "when CPU is not Intel" do
176+
it "raises an error" do
177+
allow(Hardware::CPU).to receive(:type).and_return(:arm)
178+
expect { described_class.new("10.14").requires_nehalem_cpu? }
179+
.to raise_error(ArgumentError)
180+
end
181+
end
182+
183+
it "returns false when version is null" do
184+
expect(described_class::NULL.requires_nehalem_cpu?).to be false
185+
end
106186
end
107187
end

0 commit comments

Comments
 (0)