Skip to content

Commit

Permalink
Merge pull request #4 from Invoca/STORY-18806_allow_version_request_o…
Browse files Browse the repository at this point in the history
…ptions

STORY-18806: Add max and step options for appraisal_matrix
  • Loading branch information
dcaddell authored Jul 9, 2024
2 parents 119d454 + 9941afe commit 8a84c20
Show file tree
Hide file tree
Showing 9 changed files with 238 additions and 70 deletions.
1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.1.6
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 0.2.0 - 2024-07-09
### Added
- Support special request options for `appraisal_matrix`:
- `versions`: An array of version restriction strings.
- `step`: The granularity of a release to be included in the matrix. Allowed to be :major, :minor, or :patch.

## [0.1.0] - 2024-06-26
### Added
- Add an extension to the Appraisal gem that provides an interface for generating a matrix of appraisals.
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
appraisal-matrix (0.1.0)
appraisal-matrix (0.2.0)
appraisal (~> 2.2)

GEM
Expand Down
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,27 @@ appraisal_matrix(activesupport: "6.1", sidekiq: "7.0")

In addition to specifying the minimum requested version, users will be able to make additional version requests.

#### Maximum version (Coming soon!)
#### Additional version restrictions

Include a maximum boundary (inclusive).
Include additional version boundaries. Either include the requirement strings as an array or pass the `versions` key to the options hash.
```ruby
appraisal_matrix(activesupport: { min: "6.1", max: "7.1" })
appraisal_matrix(activesupport: [">= 6.1", "< 7.1"])
appraisal_matrix(activesupport: ["~> 6.0", "!= 6.1.0"])
appraisal_matrix(activesupport: { versions: ["> 6.1.1"] })
```

#### Version step (Coming soon!)
#### Version step

The default operation is to test against each minor version. You can choose to be more or less inclusive when necessary.

Only test the latest release of each major version.
```ruby
appraisal_matrix(activesupport: { min: "6.1", max: "7.1", step: :major })
appraisal_matrix(activesupport: { versions: [">= 6.1", "< 7.1"], step: :major })
```

Or include all patch releases
```ruby
appraisal_matrix(activesupport: { versions: [">= 6.1", "< 7.1"], step: :patch })
```

## Development
Expand Down
55 changes: 41 additions & 14 deletions lib/appraisal/matrix/extensions/appraisal_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,34 @@

module Appraisal::Matrix
module AppraiseFileWithMatrix
include RubygemsHelper

# appraisal_matrix(rails: "6.0")
# appraisal_matrix(rails: "6.0", sidekiq: "5")
# appraisal_matrix(rails: "6.0", sidekiq: { min: “5.0”, max: “6.0”, step: :major })
# appraisal_matrix(rails: "6.0") do
# gem "sqlite3", "~> 2.5"
# end
class VersionArray
SUPPORTED_VERSION_STEPS = [:major, :minor, :patch].freeze

attr_reader :gem_name, :version_requirements, :step

def initialize(gem_name:, versions:, step: :minor)
SUPPORTED_VERSION_STEPS.include?(step) or raise("Unsupported version step: #{step}")

@gem_name = gem_name
@version_requirements = Gem::Requirement.new(versions)
@step = step.to_sym
end

def versions
RubygemsHelper.versions_to_test(gem_name, version_requirements, step)
end
end

# Define a matrix of appraisals to test against
# Expected usage:
# appraisal_matrix(rails: "6.0")
# appraisal_matrix(rails: "> 6.0.3")
# appraisal_matrix(rails: [">= 6.0", "< 7.1"])
# appraisal_matrix(rails: { versions: [">= 6.0", "< 7.1"], step: "major" })
# appraisal_matrix(rails: "6.0") do
# gem "sqlite3", "~> 2.5"
# end
def appraisal_matrix(**kwargs, &block)
# names_and_versions_to_test
# [
Expand All @@ -21,14 +41,21 @@ def appraisal_matrix(**kwargs, &block)
# [[a, x], [a, y], [a, z]]
# ]
names_and_versions_to_test =
kwargs.map do |gem_name, version_request|
if version_request.is_a?(Hash)
raise "TODO: Version request options not implemented yet"
else
minimum_version = Gem::Version.new(version_request)
end
kwargs.map do |gem_name, version_options|
version_array =
case version_options
when String
parsed_options = version_options.include?(" ") ? [version_options] : [">= #{version_options}"]
VersionArray.new(gem_name: gem_name, versions: parsed_options)
when Integer, Float
VersionArray.new(gem_name: gem_name, versions: [">= #{version_options}"])
when Array
VersionArray.new(gem_name: gem_name, versions: version_options)
when Hash
VersionArray.new(gem_name: gem_name, **version_options)
end

versions_to_test(gem_name, minimum_version).map do |version|
version_array.versions.map do |version|
[gem_name, version]
end
end
Expand Down
48 changes: 37 additions & 11 deletions lib/appraisal/matrix/rubygems_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,46 @@

module Appraisal
module Matrix
module RubygemsHelper
def versions_to_test(gem_name, minimum_version)
# Generate a set to store the versions to test against
versions_to_test = Set.new

# Load versions from rubygems api
URI.parse("https://rubygems.org/api/v1/versions/#{gem_name}.json").open do |raw_version_data|
JSON.parse(raw_version_data.read).each do |version_data|
version = Gem::Version.new(version_data['number'])
versions_to_test << version.segments[0..1].join('.') if version >= minimum_version && !version.prerelease?
class RubygemsHelper
class << self
# Returns a set of versions to test against for a given gem.
#
# @param gem_name [String, Symbol] The name of the gem.
# @param version_requirement [Gem::Requirement] The version requirement for the gem.
# @param step [Symbol] The step value.
#
# @return [Set] A set of versions to test against.
def versions_to_test(gem_name, version_requirement, step)
# Generate a set to store the versions to test against
versions_to_test = Set.new

# Load versions from rubygems api
URI.parse("https://rubygems.org/api/v1/versions/#{gem_name}.json").open do |raw_version_data|
JSON.parse(raw_version_data.read).each do |version_data|
version = Gem::Version.new(version_data['number'])
versions_to_test << version_for_step(version, step) if include_version?(version, version_requirement)
end
end

versions_to_test
end

versions_to_test
private

SEGMENT_STEP_SIZES = {
major: 1,
minor: 2,
patch: 3
}.freeze

def version_for_step(version, step)
size_for_step = SEGMENT_STEP_SIZES[step] or raise ArgumentError, "unsupported requested version step: #{step}, expected #{SEGMENT_STEP_SIZES.keys}"
version.segments.first(size_for_step).join(".")
end

def include_version?(version, version_requirement)
!version.prerelease? && version_requirement.satisfied_by?(version)
end
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/appraisal/matrix/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

module Appraisal
module Matrix
VERSION = "0.1.0"
VERSION = "0.2.0"
end
end
137 changes: 106 additions & 31 deletions spec/appraisal/matrix/extensions/appraisal_file_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,61 +9,136 @@
subject { appraisal_matrix(**desired_gems) }

context "with a maximum version specified" do
let(:desired_gems) { { rails: { min: "6.1", max: "7.0" } } }
before do
expect(Appraisal::Matrix::RubygemsHelper).to receive(:versions_to_test).with(:rails, Gem::Requirement.new([">= 6.1", "< 7.1"]), :minor).and_return(["6.1", "7.0"])
end

context "using the keyword argument syntax" do
let(:desired_gems) { { rails: { versions: [">= 6.1", "< 7.1"] } } }

it "creates a matrix of appraisals" do
expect(self).to receive(:appraise).with("rails-6_1").and_yield { |block_scope| expect(block_scope).to receive(:gem).with(:rails, "~> 6.1.0") }
expect(self).to receive(:appraise).with("rails-7_0").and_yield { |block_scope| expect(block_scope).to receive(:gem).with(:rails, "~> 7.0.0") }
subject
end
end

context "using the array argument syntax" do
let (:desired_gems) { { rails: [">= 6.1", "< 7.1"] } }

it "is not implemented yet" do
expect { subject }.to raise_error("TODO: Version request options not implemented yet")
it "creates a matrix of appraisals" do
expect(self).to receive(:appraise).with("rails-6_1").and_yield { |block_scope| expect(block_scope).to receive(:gem).with(:rails, "~> 6.1.0") }
expect(self).to receive(:appraise).with("rails-7_0").and_yield { |block_scope| expect(block_scope).to receive(:gem).with(:rails, "~> 7.0.0") }
subject
end
end
end

context "with a step specified" do
let(:desired_gems) { { rails: { min: "6.1", step: :major } } }
context "requesting major version steps" do
let(:desired_gems) { { rails: { versions: [">= 6.1"], step: :major } } }

before do
expect(Appraisal::Matrix::RubygemsHelper).to receive(:versions_to_test).with(:rails, Gem::Requirement.new([">= 6.1"]), :major).and_return(["6", "7"])
end

it "is not implemented yet" do
expect { subject }.to raise_error("TODO: Version request options not implemented yet")
it "creates a matrix of appraisals" do
expect(self).to receive(:appraise).with("rails-6").and_yield { |block_scope| expect(block_scope).to receive(:gem).with(:rails, "~> 6.0") }
expect(self).to receive(:appraise).with("rails-7").and_yield { |block_scope| expect(block_scope).to receive(:gem).with(:rails, "~> 7.0") }
subject
end
end

context "for a single gem" do
let(:desired_gems) { { rails: "6.1" } }
context "requesting patch version steps" do
let(:desired_gems) { { rails: { versions: [">= 6.1"], step: :patch } } }

before do
allow(self).to receive(:versions_to_test) { ["6.1", "7.0", "7.1"] }
expect(Appraisal::Matrix::RubygemsHelper).to receive(:versions_to_test).with(:rails, Gem::Requirement.new([">= 6.1"]), :patch).and_return(["6.1.0", "6.1.1", "7.0.0", "7.1.0"])
end

it "creates a matrix of appraisals including the specified minimum version" do
expect(self).to receive(:appraise).with("rails-6_1").and_yield { |block_scope| expect(block_scope).to receive(:gem).with(:rails, "~> 6.1.0") }
expect(self).to receive(:appraise).with("rails-7_0").and_yield { |block_scope| expect(block_scope).to receive(:gem).with(:rails, "~> 7.0.0") }
expect(self).to receive(:appraise).with("rails-7_1").and_yield { |block_scope| expect(block_scope).to receive(:gem).with(:rails, "~> 7.1.0") }
it "creates a matrix of appraisals" do
expect(self).to receive(:appraise).with("rails-6_1_0").and_yield { |block_scope| expect(block_scope).to receive(:gem).with(:rails, "~> 6.1.0.0") }
expect(self).to receive(:appraise).with("rails-6_1_1").and_yield { |block_scope| expect(block_scope).to receive(:gem).with(:rails, "~> 6.1.1.0") }
expect(self).to receive(:appraise).with("rails-7_0_0").and_yield { |block_scope| expect(block_scope).to receive(:gem).with(:rails, "~> 7.0.0.0") }
expect(self).to receive(:appraise).with("rails-7_1_0").and_yield { |block_scope| expect(block_scope).to receive(:gem).with(:rails, "~> 7.1.0.0") }
subject
end
end

context "with a block to pass into each appraisal" do
it "yields the block to each appraisal" do
expect(self).to receive(:appraise).with("rails-6_1").and_yield do |block_scope|
expect(block_scope).to receive(:gem).with(:rails, "~> 6.1.0")
expect(block_scope).to receive(:gem).with("sqlite3", "~> 2.5")
end
expect(self).to receive(:appraise).with("rails-7_0").and_yield do |block_scope|
expect(block_scope).to receive(:gem).with(:rails, "~> 7.0.0")
expect(block_scope).to receive(:gem).with("sqlite3", "~> 2.5")
end
expect(self).to receive(:appraise).with("rails-7_1").and_yield do |block_scope|
expect(block_scope).to receive(:gem).with(:rails, "~> 7.1.0")
expect(block_scope).to receive(:gem).with("sqlite3", "~> 2.5")
end
context "requesting a step that is not supported" do
let(:desired_gems) { { rails: { versions: [">= 6.1"], step: :pizza } } }

it "raises an error" do
expect { subject }.to raise_error("Unsupported version step: pizza")
end
end

appraisal_matrix(**desired_gems) { gem "sqlite3", "~> 2.5" }
context "default behavior for a single gem" do
shared_examples "a matrix of appraisals" do
it "creates a matrix of appraisals including the specified minimum version" do
expect(self).to receive(:appraise).with("rails-6_1").and_yield { |block_scope| expect(block_scope).to receive(:gem).with(:rails, "~> 6.1.0") }
expect(self).to receive(:appraise).with("rails-7_0").and_yield { |block_scope| expect(block_scope).to receive(:gem).with(:rails, "~> 7.0.0") }
expect(self).to receive(:appraise).with("rails-7_1").and_yield { |block_scope| expect(block_scope).to receive(:gem).with(:rails, "~> 7.1.0") }
subject
end
end

before do
expect(Appraisal::Matrix::RubygemsHelper).to receive(:versions_to_test).with(:rails, expected_version_requirement, :minor).and_return(["6.1", "7.0", "7.1"])
end

let(:expected_version_requirement) { Gem::Requirement.new([">= 6.1"]) }

context "as a string" do
let(:desired_gems) { { rails: "6.1" } }

it_behaves_like "a matrix of appraisals"

context "with a block to pass into each appraisal" do
it "yields the block to each appraisal" do
expect(self).to receive(:appraise).with("rails-6_1").and_yield do |block_scope|
expect(block_scope).to receive(:gem).with(:rails, "~> 6.1.0")
expect(block_scope).to receive(:gem).with("sqlite3", "~> 2.5")
end
expect(self).to receive(:appraise).with("rails-7_0").and_yield do |block_scope|
expect(block_scope).to receive(:gem).with(:rails, "~> 7.0.0")
expect(block_scope).to receive(:gem).with("sqlite3", "~> 2.5")
end
expect(self).to receive(:appraise).with("rails-7_1").and_yield do |block_scope|
expect(block_scope).to receive(:gem).with(:rails, "~> 7.1.0")
expect(block_scope).to receive(:gem).with("sqlite3", "~> 2.5")
end

appraisal_matrix(**desired_gems) { gem "sqlite3", "~> 2.5" }
end
end
end

context "as a string with an operator provided" do
let(:desired_gems) { { rails: ">= 6.1" } }

it_behaves_like "a matrix of appraisals"
end

context "as a Float" do
let(:desired_gems) { { rails: 6.1 } }

it_behaves_like "a matrix of appraisals"
end

context "as an Integer" do
let(:desired_gems) { { rails: 6 } }
let(:expected_version_requirement) { Gem::Requirement.new([">= 6"]) }

it_behaves_like "a matrix of appraisals"
end
end

context "for multiple gems" do
let(:desired_gems) { { rails: "6.1", sidekiq: "5" } }

before do
allow(self).to receive(:versions_to_test).with(:rails, Gem::Version.new("6.1")).and_return(["6.1", "7.0", "7.1"])
allow(self).to receive(:versions_to_test).with(:sidekiq, Gem::Version.new("5")).and_return(["5.0", "6.0"])
expect(Appraisal::Matrix::RubygemsHelper).to receive(:versions_to_test).with(:rails, Gem::Requirement.new([">= 6.1"]), :minor).and_return(["6.1", "7.0", "7.1"])
expect(Appraisal::Matrix::RubygemsHelper).to receive(:versions_to_test).with(:sidekiq, Gem::Requirement.new([">= 5"]), :minor).and_return(["5.0", "6.0"])
end

it "creates a matrix of appraisals including the specified minimum version" do
Expand Down
Loading

0 comments on commit 8a84c20

Please sign in to comment.