diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..9cec716 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.1.6 diff --git a/CHANGELOG.md b/CHANGELOG.md index e685ece..4cb64c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Gemfile.lock b/Gemfile.lock index 8afa3d3..29c4817 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - appraisal-matrix (0.1.0) + appraisal-matrix (0.2.0) appraisal (~> 2.2) GEM diff --git a/README.md b/README.md index 10c9778..8a4715e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/appraisal/matrix/extensions/appraisal_file.rb b/lib/appraisal/matrix/extensions/appraisal_file.rb index 4a349aa..f4f7127 100644 --- a/lib/appraisal/matrix/extensions/appraisal_file.rb +++ b/lib/appraisal/matrix/extensions/appraisal_file.rb @@ -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 # [ @@ -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 diff --git a/lib/appraisal/matrix/rubygems_helper.rb b/lib/appraisal/matrix/rubygems_helper.rb index 4a9cd41..16ca29d 100644 --- a/lib/appraisal/matrix/rubygems_helper.rb +++ b/lib/appraisal/matrix/rubygems_helper.rb @@ -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 diff --git a/lib/appraisal/matrix/version.rb b/lib/appraisal/matrix/version.rb index 1476589..6b0e693 100644 --- a/lib/appraisal/matrix/version.rb +++ b/lib/appraisal/matrix/version.rb @@ -2,6 +2,6 @@ module Appraisal module Matrix - VERSION = "0.1.0" + VERSION = "0.2.0" end end diff --git a/spec/appraisal/matrix/extensions/appraisal_file_spec.rb b/spec/appraisal/matrix/extensions/appraisal_file_spec.rb index 94bde59..2e698ec 100644 --- a/spec/appraisal/matrix/extensions/appraisal_file_spec.rb +++ b/spec/appraisal/matrix/extensions/appraisal_file_spec.rb @@ -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 diff --git a/spec/appraisal/matrix/rubygems_helper_spec.rb b/spec/appraisal/matrix/rubygems_helper_spec.rb index 37e12d1..81d5333 100644 --- a/spec/appraisal/matrix/rubygems_helper_spec.rb +++ b/spec/appraisal/matrix/rubygems_helper_spec.rb @@ -3,13 +3,12 @@ require_relative "../../../lib/appraisal/matrix/rubygems_helper" RSpec.describe Appraisal::Matrix::RubygemsHelper do - include Appraisal::Matrix::RubygemsHelper - describe "#versions_to_test" do - subject { versions_to_test(gem_name, minimum_version) } + subject { described_class.versions_to_test(gem_name, version_requirements, step) } - let(:gem_name) { "rails" } - let(:minimum_version) { Gem::Version.new("6.0") } + let(:gem_name) { "rails" } + let(:version_requirements) { Gem::Requirement.new(['>= 6.0']) } + let(:step) { :minor } let(:parsed_uri) { double("uri") } let(:raw_version_data) do @@ -20,6 +19,7 @@ { "number" => "6.0.0.rc.1" }, { "number" => "6.0.0" }, { "number" => "6.1.0" }, + { "number" => "6.1.1" }, { "number" => "7.0.0.rc.1" }, { "number" => "7.0.0" }, { "number" => "7.1.0" }, @@ -33,8 +33,32 @@ allow(parsed_uri).to receive(:open).and_yield(raw_version_data) end - it "returns versions to test" do - expect(subject).to eq(Set.new(["6.0", "6.1", "7.0", "7.1"])) + it { is_expected.to eq(Set.new(["6.0", "6.1", "7.0", "7.1"])) } + + context "when a maximum version is specified" do + let(:version_requirements) { Gem::Requirement.new(['>= 6.0', '< 7.0']) } + + it { is_expected.to eq(Set.new(["6.0", "6.1"])) } + end + + context "when the step is :major" do + let(:step) { :major } + + it { is_expected.to eq(Set.new(["6", "7"])) } + end + + context "when the step is :patch" do + let(:step) { :patch } + + it { is_expected.to eq(Set.new(["6.0.0", "6.1.0", "6.1.1", "7.0.0", "7.1.0", "7.1.1"])) } + end + + context "when the step is anything else" do + let(:step) { :pizza } + + it "raises an error" do + expect { subject }.to raise_error("unsupported requested version step: pizza, expected [:major, :minor, :patch]") + end end end end