diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index c98a8a0c9fd..8dd63a297be 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -14,7 +14,7 @@ concurrency: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SMOKE_TEST_BRANCH: main + SMOKE_TEST_BRANCH: revert-228-amazimbe/use-new-maven-version jobs: discover: runs-on: ubuntu-latest diff --git a/maven/lib/dependabot/maven/new_version.rb b/maven/lib/dependabot/maven/new_version.rb new file mode 100644 index 00000000000..592d7eedaeb --- /dev/null +++ b/maven/lib/dependabot/maven/new_version.rb @@ -0,0 +1,71 @@ +# typed: strict +# frozen_string_literal: true + +require "dependabot/maven/version_parser" +require "dependabot/version" +require "dependabot/utils" + +# See https://maven.apache.org/pom.html#Version_Order_Specification for details. + +module Dependabot + module Maven + class NewVersion + extend T::Sig + extend T::Helpers + + PRERELEASE_QUALIFIERS = T.let([ + Dependabot::Maven::VersionParser::ALPHA, + Dependabot::Maven::VersionParser::BETA, + Dependabot::Maven::VersionParser::MILESTONE, + Dependabot::Maven::VersionParser::RC, + Dependabot::Maven::VersionParser::SNAPSHOT + ].freeze, T::Array[Integer]) + + sig { returns(Dependabot::Maven::TokenBucket) } + attr_accessor :token_bucket + + sig { params(version: String).returns(T::Boolean) } + def self.correct?(version) + return false if version.empty? + + Dependabot::Maven::VersionParser.parse(version.to_s).to_a.any? + rescue Dependabot::BadRequirementError + Dependabot.logger.info("Malformed version string - #{version}") + false + end + + sig { params(version: String).void } + def initialize(version) + @version_string = T.let(version, String) + @token_bucket = T.let(Dependabot::Maven::VersionParser.parse(version), Dependabot::Maven::TokenBucket) + end + + sig { returns(String) } + def inspect + "#<#{self.class} #{version_string}>" + end + + sig { returns(String) } + def to_s + version_string + end + + sig { returns(T::Boolean) } + def prerelease? + token_bucket.to_a.flatten.any? do |token| + token.is_a?(Integer) && token.negative? + end + end + + sig { params(other: ::Dependabot::Maven::NewVersion).returns(Integer) } + def <=>(other) + T.must(token_bucket <=> other.token_bucket) + end + + private + + sig { returns(String) } + attr_reader :version_string + end + end +end diff --git a/maven/lib/dependabot/maven/update_checker/requirements_updater.rb b/maven/lib/dependabot/maven/update_checker/requirements_updater.rb index 25ee2267b6b..8d3774edf63 100644 --- a/maven/lib/dependabot/maven/update_checker/requirements_updater.rb +++ b/maven/lib/dependabot/maven/update_checker/requirements_updater.rb @@ -50,8 +50,12 @@ def updated_requirements attr_reader :properties_to_update def update_requirement(req_string) - # Since range requirements are excluded this must be exact - update_exact_requirement(req_string) + if req_string.include?(".+") + update_dynamic_requirement(req_string) + else + # Since range requirements are excluded this must be exact + update_exact_requirement(req_string) + end end def update_exact_requirement(req_string) @@ -60,6 +64,16 @@ def update_exact_requirement(req_string) req_string.gsub(old_version.to_s, latest_version.to_s) end + # This is really only a Gradle thing, but Gradle relies on this + # RequirementsUpdater too + def update_dynamic_requirement(req_string) + precision = req_string.split(".").take_while { |s| s != "+" }.count + + version_parts = latest_version.segments.first(precision) + + version_parts.join(".") + ".+" + end + def version_class Maven::Version end diff --git a/maven/lib/dependabot/maven/version.rb b/maven/lib/dependabot/maven/version.rb index ba41088998c..0cd26fe8f81 100644 --- a/maven/lib/dependabot/maven/version.rb +++ b/maven/lib/dependabot/maven/version.rb @@ -1,80 +1,192 @@ -# typed: strict +# typed: true # frozen_string_literal: true -require "dependabot/maven/version_parser" require "dependabot/version" require "dependabot/utils" +# Java versions use dots and dashes when tokenising their versions. +# Gem::Version converts a "-" to ".pre.", so we override the `to_s` method. +# # See https://maven.apache.org/pom.html#Version_Order_Specification for details. module Dependabot module Maven class Version < Dependabot::Version - extend T::Sig - extend T::Helpers - - PRERELEASE_QUALIFIERS = T.let([ - Dependabot::Maven::VersionParser::ALPHA, - Dependabot::Maven::VersionParser::BETA, - Dependabot::Maven::VersionParser::MILESTONE, - Dependabot::Maven::VersionParser::RC, - Dependabot::Maven::VersionParser::SNAPSHOT - ].freeze, T::Array[Integer]) - + NULL_VALUES = %w(0 final ga).freeze + PREFIXED_TOKEN_HIERARCHY = { + "." => { qualifier: 1, number: 4 }, + "-" => { qualifier: 2, number: 3 }, + "+" => { qualifier: 3, number: 2 } + }.freeze + NAMED_QUALIFIERS_HIERARCHY = { + "a" => 1, "alpha" => 1, + "b" => 2, "beta" => 2, + "m" => 3, "milestone" => 3, + "rc" => 4, "cr" => 4, "pr" => 4, "pre" => 4, + "snapshot" => 5, "dev" => 5, + "ga" => 6, "" => 6, "final" => 6, + "sp" => 7 + }.freeze VERSION_PATTERN = "[0-9a-zA-Z]+" \ '(?>\.[0-9a-zA-Z]*)*' \ '([_\-\+][0-9A-Za-z_-]*(\.[0-9A-Za-z_-]*)*)?' + ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})?\s*\z/ - sig { returns(Dependabot::Maven::TokenBucket) } - attr_accessor :token_bucket - - sig { override.params(version: VersionParameter).returns(T::Boolean) } def self.correct?(version) - return false if version.to_s.empty? + return false if version.nil? - Dependabot::Maven::VersionParser.parse(version.to_s).to_a.any? - rescue ArgumentError - Dependabot.logger.info("Malformed version string #{version}") - false + version.to_s.match?(ANCHORED_VERSION_PATTERN) end - sig { override.params(version: VersionParameter).void } def initialize(version) - raise BadRequirementError, "Malformed version string - string is nil" if version.nil? - - @version_string = T.let(version.to_s, String) - @token_bucket = T.let(Dependabot::Maven::VersionParser.parse(version_string), Dependabot::Maven::TokenBucket) + @version_string = version.to_s super(version.to_s.tr("_", "-")) end - sig { returns(String) } def inspect - "#<#{self.class} #{version_string}>" + "#<#{self.class} #{@version_string}>" end - sig { returns(String) } def to_s - version_string + @version_string end - sig { returns(T::Boolean) } def prerelease? - token_bucket.to_a.flatten.any? do |token| - token.is_a?(Integer) && token.negative? + tokens.any? do |token| + next true if token == "eap" + next false unless NAMED_QUALIFIERS_HIERARCHY[token] + + NAMED_QUALIFIERS_HIERARCHY[token] < 6 end end - sig { params(other: VersionParameter).returns(Integer) } def <=>(other) - other = Dependabot::Maven::Version.new(other.to_s) unless other.is_a? Dependabot::Maven::Version - T.must(token_bucket <=> T.cast(other, Dependabot::Maven::Version).token_bucket) + version = stringify_version(@version_string) + version = fill_tokens(version) + version = trim_version(version) + + other_version = stringify_version(other) + other_version = fill_tokens(other_version) + other_version = trim_version(other_version) + + version, other_version = convert_dates(version, other_version) + + prefixed_tokens = split_into_prefixed_tokens(version) + other_prefixed_tokens = split_into_prefixed_tokens(other_version) + + prefixed_tokens, other_prefixed_tokens = + pad_for_comparison(prefixed_tokens, other_prefixed_tokens) + + prefixed_tokens.count.times.each do |index| + comp = compare_prefixed_token( + prefix: prefixed_tokens[index][0], + token: prefixed_tokens[index][1..-1] || "", + other_prefix: other_prefixed_tokens[index][0], + other_token: other_prefixed_tokens[index][1..-1] || "" + ) + return comp unless comp.zero? + end + + 0 end private - sig { returns(String) } - attr_reader :version_string + def tokens + @tokens ||= + begin + version = @version_string.to_s.downcase + version = fill_tokens(version) + version = trim_version(version) + split_into_prefixed_tokens(version).map { |t| t[1..-1] } + end + end + + def stringify_version(version) + version = version.to_s.downcase + + # Not technically correct, but pragmatic + version.gsub(/^v(?=\d)/, "") + end + + def fill_tokens(version) + # Add separators when transitioning from digits to characters + version = version.gsub(/(\d)([A-Za-z])/, '\1-\2') + version = version.gsub(/([A-Za-z])(\d)/, '\1-\2') + + # Replace empty tokens with 0 + version = version.gsub(/([\.\-])([\.\-])/, '\10\2') + version = version.gsub(/^([\.\-])/, '0\1') + version.gsub(/([\.\-])$/, '\10') + end + + def trim_version(version) + version.split("-").filter_map do |v| + parts = v.split(".") + parts = parts[0..-2] while NULL_VALUES.include?(parts&.last) + parts&.join(".") + end.reject(&:empty?).join("-") + end + + def convert_dates(version, other_version) + default = [version, other_version] + return default unless version.match?(/^\d{4}-?\d{2}-?\d{2}$/) + return default unless other_version.match?(/^\d{4}-?\d{2}-?\d{2}$/) + + [version.delete("-"), other_version.delete("-")] + end + + def split_into_prefixed_tokens(version) + ".#{version}".split(/(?=[\-\.\+])/) + end + + def pad_for_comparison(prefixed_tokens, other_prefixed_tokens) + prefixed_tokens = prefixed_tokens.dup + other_prefixed_tokens = other_prefixed_tokens.dup + + longest = [prefixed_tokens, other_prefixed_tokens].max_by(&:count) + shortest = [prefixed_tokens, other_prefixed_tokens].min_by(&:count) + + longest.count.times do |index| + next unless shortest[index].nil? + + shortest[index] = longest[index].start_with?(".") ? ".0" : "-" + end + + [prefixed_tokens, other_prefixed_tokens] + end + + def compare_prefixed_token(prefix:, token:, other_prefix:, other_token:) + token_type = token.match?(/^\d+$/) ? :number : :qualifier + other_token_type = other_token.match?(/^\d+$/) ? :number : :qualifier + + hierarchy = PREFIXED_TOKEN_HIERARCHY.fetch(prefix).fetch(token_type) + other_hierarchy = + PREFIXED_TOKEN_HIERARCHY.fetch(other_prefix).fetch(other_token_type) + + hierarchy_comparison = hierarchy <=> other_hierarchy + return hierarchy_comparison unless hierarchy_comparison.zero? + + compare_token(token: token, other_token: other_token) + end + + def compare_token(token:, other_token:) + if (token_hierarchy = NAMED_QUALIFIERS_HIERARCHY[token]) + return -1 unless NAMED_QUALIFIERS_HIERARCHY[other_token] + + return token_hierarchy <=> NAMED_QUALIFIERS_HIERARCHY[other_token] + end + + return 1 if NAMED_QUALIFIERS_HIERARCHY[other_token] + + if token.match?(/\A\d+\z/) && other_token.match?(/\A\d+\z/) + token = token.to_i + other_token = other_token.to_i + end + + token <=> other_token + end end end end diff --git a/maven/spec/dependabot/maven/new_version_spec.rb b/maven/spec/dependabot/maven/new_version_spec.rb new file mode 100644 index 00000000000..ad0dddee556 --- /dev/null +++ b/maven/spec/dependabot/maven/new_version_spec.rb @@ -0,0 +1,503 @@ +# typed: false +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/maven/new_version" + +RSpec.describe Dependabot::Maven::NewVersion do + subject(:version) { described_class.new(version_string) } + + let(:version_string) { "1.0.0" } + + describe ".correct?" do + subject { described_class.correct?(version_string) } + + context "with a normal version" do + let(:version_string) { "1.0.0" } + + it { is_expected.to be(true) } + end + + context "with a normal version" do + let(:version_string) { "Finchley" } + + it { is_expected.to be(true) } + end + + context "with a dynamic version" do + let(:version_string) { "1.+" } + + it { is_expected.to be(true) } + end + + context "with an empty string" do + let(:version_string) { "" } + + it { is_expected.to be(false) } + end + + context "with a malformed version string" do + let(:version_string) { "-" } + + it { is_expected.to be(false) } + end + end + + describe "#to_s" do + subject { version.to_s } + + context "with no dashes" do + let(:version_string) { "1.0.0" } + + it { is_expected.to eq("1.0.0") } + end + + context "with a + separated build number" do + let(:version_string) { "1.0.0+100" } + + it { is_expected.to eq("1.0.0+100") } + end + + context "with a + separated alphanumeric build identifier" do + let(:version_string) { "1.0.0+build1" } + + it { is_expected.to eq("1.0.0+build1") } + end + + context "with a dot-specified prerelease" do + let(:version_string) { "1.0.0.pre1" } + + it { is_expected.to eq("1.0.0.pre1") } + end + + context "with a dash-specified prerelease" do + let(:version_string) { "1.0.0-pre1" } + + it { is_expected.to eq("1.0.0-pre1") } + end + + context "with an underscore-specified prerelease" do + let(:version_string) { "1.0.0_pre1" } + + it { is_expected.to eq("1.0.0_pre1") } + end + + context "with space as version" do + let(:version_string) { "" } + let(:err_msg) { "Malformed version string - string is empty" } + + it "raises an exception" do + expect { version }.to raise_error(Dependabot::BadRequirementError, err_msg) + end + end + + context "with a dot as version" do + let(:version_string) { "." } + let(:err_msg) { "Malformed version string - #{version_string}" } + + it "raises an exception" do + expect { version }.to raise_error(Dependabot::BadRequirementError, err_msg) + end + end + + context "with a hyphen as a version" do + let(:version_string) { "-" } + let(:err_msg) { "Malformed version string - #{version_string}" } + + it "raises an exception" do + expect { version }.to raise_error(Dependabot::BadRequirementError, err_msg) + end + end + end + + describe "#prerelease?" do + subject { version.prerelease? } + + context "with an alpha" do + let(:version_string) { "1.0.0-alpha" } + + it { is_expected.to be(true) } + end + + context "with a capitalised alpha" do + let(:version_string) { "1.0.0-Alpha" } + + it { is_expected.to be(true) } + end + + context "with an alpha separated with a ." do + let(:version_string) { "1.0.0.alpha" } + + it { is_expected.to be(true) } + end + + context "with an alpha with no separator" do + let(:version_string) { "1.0.0alpha" } + + it { is_expected.to be(true) } + end + + context "with an alligator" do + let(:version_string) { "1.0.0alligator" } + + it { is_expected.to be(false) } + end + + context "with a release" do + let(:version_string) { "1.0.0" } + + it { is_expected.to be(false) } + end + + context "with a post-release" do + let(:version_string) { "1.0.0.sp7" } + + it { is_expected.to be(false) } + end + end + + describe "#inspect" do + subject { described_class.new(version_string).inspect } + + let(:version_string) { "1.0.0+build1" } + + it { is_expected.to eq("#<#{described_class} #{version_string}>") } + end + + describe "#<=>" do + subject { version.send(:<=>, other_version) } + + context "with semantic versions" do + let(:versions) do + [ + ["1.2.3", "1.2.2", 1], + ["1.2.3", "1.2.3", 0], + ["1.2.3", "v1.2.3", 0] + ] + end + + it "returns the correct result" do + versions.each do |input| + version1, version2, result = input + version = described_class.new(version1) + other_version = described_class.new(version2) + expect(version <=> other_version).to eq(result) + expect(other_version <=> version).to eq(-result) + end + end + end + + context "with semantic versions that have a build number" do + let(:versions) do + [ + ["1.2.3", "1.2.3-1", -1], + ["1.2.3", "1.2.3-0-1", -1], + ["1.2.3", "1.2.3-0.1", -1], + ["1.2.3-2", "1.2.3-1", 1], + ["1.2.3-0.2", "1.2.3-1.1", -1] + ] + end + + it "returns the correct result" do + versions.each do |input| + version1, version2, result = input + version = described_class.new(version1) + other_version = described_class.new(version2) + expect(version <=> other_version).to eq(result) + expect(other_version <=> version).to eq(-result) + end + end + end + + context "with semantic versions that have a qualifier" do + let(:versions) do + [ + ["1.2.3", "1.2.3-a0", 1], # alpha has lower precedence + ["1.2.3", "1.2.3-a", -1] # 'a' without a following int is not alpha + + ] + end + + it "returns the correct result" do + versions.each do |input| + version1, version2, result = input + version = described_class.new(version1) + other_version = described_class.new(version2) + expect(version <=> other_version).to eq(result) + expect(other_version <=> version).to eq(-result) + end + end + end + + context "with versions that have trailing nulls" do + let(:versions) do + [ + ["1alpha-0", "1alpha", 0], + ["1alpha-0", "1alpha0", 0], + ["1alpha-0", "1alpha.0", 0], + ["1alpha-0", "1alpha.z", -1], + + ["1beta-0", "1beta", 0], + ["1beta-0", "1beta0", 0], + ["1beta-0", "1beta.0", 0], + ["1beta-0", "1beta.z", -1], + + ["1rc-0", "1rc0", 0], + ["1rc-0", "1rc", 0], + ["1rc-0", "1rc.0", 0], + ["1rc-0", "1rc.z", -1], + + ["1sp-0", "1sp0", 0], + ["1sp-0", "1sp", 0], + ["1sp-0", "1sp.0", 0], + ["1sp-0", "1sp.z", -1], + + ["1.ga", "1-ga", 0], + ["1.0", "1.ga", 0], + ["1.0.FINAL", "1", 0], + ["1.0", "1.release", 0], + + ["1.2.0", "1.2", 0], + ["1.2.3", "1.2.3-0", 0], + ["1.2.3-0", "1.2.3-0", 0], + ["1.2.3-0", "1.2.3-a0", 1], + ["1.2.3-0", "1.2.3-a", -1], + ["1.2.3-0", "1.2.3-1", -1], + ["1.2.3-0", "1.2.3-0-1", -1], + + ["1snapshot-0", "1snapshot0", 0], + ["1snapshot-0", "1snapshot", 0], + ["1snapshot-0", "1snapshot.0", 0], + ["1snapshot-0", "1snapshot.z", -1], + + ["1milestone-0", "1milestone", 0], + ["1milestone-0", "1milestone0", 0], + ["1milestone-0", "1milestone.0", 0], + ["1milestone-0", "1milestone.z", -1] + ] + end + + it "returns the correct result" do + versions.each do |input| + version1, version2, result = input + version = described_class.new(version1) + other_version = described_class.new(version2) + expect(version <=> other_version).to eq(result) + expect(other_version <=> version).to eq(-result) + end + end + end + + context "with equivalent shortened qualifiers" do + let(:versions) do + [ + ["1alpha-0", "1a0", 0], + ["1beta-0", "1b0", 0], + ["1milestone-0", "1m0", 0] + ] + end + + it "returns 0" do + versions.each do |input| + version1, version2, result = input + version = described_class.new(version1) + other_version = described_class.new(version2) + expect(version <=> other_version).to eq(result) + expect(other_version <=> version).to eq(-result) + end + end + end + + context "with dot, hyphen and digit / qualifier transitions as separators" do + let(:versions) do + [ + ["1alpha.z", "1alpha-z", 0], + ["1alpha1", "1alpha-1", 0], + ["1alpha-1", "1alpha.1", -1], + ["1beta.z", "1beta-z", 0], + ["1beta1", "1beta-1", 0], + ["1beta-1", "1beta.1", -1], + ["1-a", "1a", 0], + ["1-a", "1.a", 0], + ["1-b", "1-b-1", -1], + ["1-b-1", "1-b.1", -1], + ["1sp.z", "1sp-z", 0], + ["1sp1", "1sp-1", 0], + ["1sp-1", "1sp.1", -1], + ["1rc.z", "1rc-z", 0], + ["1rc1", "1rc-1", 0], + ["1rc-1", "1rc.1", -1], + ["1milestone.z", "1milestone-z", 0], + ["1milestone1", "1milestone-1", 0], + ["1milestone-1", "1milestone.1", -1], + ["1snapshot.z", "1snapshot-z", 0], + ["1snapshot1", "1snapshot-1", 0], + ["1snapshot-1", "1snapshot.1", -1] + ] + end + + it "returns 0" do + versions.each do |input| + version1, version2, result = input + version = described_class.new(version1) + other_version = described_class.new(version2) + expect(version <=> other_version).to eq(result) + expect(other_version <=> version).to eq(-result) + end + end + end + + context "with qualifiers with different precedence" do + let(:versions) do + [ + ["1alpha.1", "1beta.1", -1], + ["1beta.1", "1milestone.1", -1], + ["1milestone.1", "1rc.1", -1], + ["1rc.1", "1snapshot.1", -1], + ["1.sp", "1.ga", 1], + ["1.release", "1.ga", 0] + ] + end + + it "returns the correct value" do + versions.each do |input| + version1, version2, result = input + version = described_class.new(version1) + other_version = described_class.new(version2) + expect(version <=> other_version).to eq(result) + expect(other_version <=> version).to eq(-result) + end + end + end + + context "with equivalent qualifiers cr and rc" do + let(:version) { described_class.new("1.0rc-1") } + let(:other_version) { described_class.new("1.0-cr1") } + + it "returns 0" do + expect(version <=> other_version).to eq(0) + expect(other_version <=> version).to eq(0) + end + end + + context "when comparing alphanumerically" do + let(:versions) do + [ + ["1alpha-z", "1alpha1", -1], + ["1beta-z", "1beta1", -1], + ["1milestone-z", "1milestone1", -1], + ["1rc-z", "1rc1", -1], + ["1snapshot-z", "1snapshot1", -1], + ["1sp-z", "1sp1", -1], + ["181", "DEV", 1] + ] + end + + it "gives higher precedence to digits" do + versions.each do |input| + version1, version2, result = input + version = described_class.new(version1) + other_version = described_class.new(version2) + expect(version <=> other_version).to eq(result) + expect(other_version <=> version).to eq(-result) + end + end + end + + context "when comparing alphabetically" do + let(:versions) do + [ + ["1-a", "1-b", -1], + ["Finchley", "Edgware", 1], + ["1.something", "1.SOMETHING", 0] + ] + end + + it "returns the correct result" do + versions.each do |input| + version1, version2, result = input + version = described_class.new(version1) + other_version = described_class.new(version2) + expect(version <=> other_version).to eq(result) + expect(other_version <=> version).to eq(-result) + end + end + end + + context "when comparing numerically" do + let(:versions) do + [ + ["1-b.1", "1-b.2", -1], + ["9.0.0+102", "9.0.0+91", 1], + ["1-foo2", "1-foo10", -1] + + ] + end + + it "returns the correct result" do + versions.each do |input| + version1, version2, result = input + version = described_class.new(version1) + other_version = described_class.new(version2) + expect(version <=> other_version).to eq(result) + expect(other_version <=> version).to eq(-result) + end + end + end + + context "when comparing padded versions" do + let(:versions) do + [ + ["1snapshot.1", "1", -1], + ["1-snapshot", "1", -1], + ["1", "1sp0", -1], + ["1sp.1", "1-a", -1], + ["1", "1.1", -1], + ["1", "1-sp", -1], + ["1-ga-1", "1-1", -1] + ] + end + + it "returns the correct result" do + versions.each do |input| + version1, version2, result = input + version = described_class.new(version1) + other_version = described_class.new(version2) + expect(version <=> other_version).to eq(result) + expect(other_version <=> version).to eq(-result) + end + end + end + + context "when ordering versions" do + let(:versions) do + [ + described_class.new("NotAVersionSting"), + described_class.new("1.0-alpha"), + described_class.new("1.0a1-SNAPSHOT"), + described_class.new("1.0-alpha1"), + described_class.new("1.0beta1-SNAPSHOT"), + described_class.new("1.0-b2"), + described_class.new("1.0-beta3.SNAPSHOT"), + described_class.new("1.0-beta3"), + described_class.new("1.0-milestone1-SNAPSHOT"), + described_class.new("1.0-m2"), + described_class.new("1.0-rc1-SNAPSHOT"), + described_class.new("1.0-cr1"), + described_class.new("1.0-SNAPSHOT"), + described_class.new("1.0-RELEASE"), + described_class.new("1.0-sp"), + described_class.new("1.0-a"), + described_class.new("1.0-whatever"), + described_class.new("1.0.z"), + described_class.new("1.0.1"), + described_class.new("1.0.1.0.0.0.0.0.0.0.0.0.0.0.1") + ] + end + + it "sorts versions correctly" do + expect(versions.shuffle.sort).to eq(versions) + end + end + end +end diff --git a/maven/spec/dependabot/maven/requirement_spec.rb b/maven/spec/dependabot/maven/requirement_spec.rb index 1d53a79979e..064377ccb8e 100644 --- a/maven/spec/dependabot/maven/requirement_spec.rb +++ b/maven/spec/dependabot/maven/requirement_spec.rb @@ -16,7 +16,7 @@ context "with a pre-release version" do let(:requirement_string) { "1.3.alpha" } - it { is_expected.to be_satisfied_by(Gem::Version.new("1.3.alpha")) } + it { is_expected.to be_satisfied_by(Gem::Version.new("1.3.a")) } end context "with a version that wouldn't be a valid Gem::Version" do diff --git a/maven/spec/dependabot/maven/update_checker/requirements_updater_spec.rb b/maven/spec/dependabot/maven/update_checker/requirements_updater_spec.rb index e017876393d..50de6222761 100644 --- a/maven/spec/dependabot/maven/update_checker/requirements_updater_spec.rb +++ b/maven/spec/dependabot/maven/update_checker/requirements_updater_spec.rb @@ -80,6 +80,12 @@ its([:requirement]) { is_expected.to eq("[23.6-jre]") } end + context "when a dynamic requirement was previously specified" do + let(:pom_req_string) { "22.+" } + + its([:requirement]) { is_expected.to eq("23.+") } + end + context "when there were multiple requirements" do let(:requirements) { [pom_req, other_pom_req] } diff --git a/maven/spec/dependabot/maven/version_spec.rb b/maven/spec/dependabot/maven/version_spec.rb index 0e5dc0ee82f..934c90779ea 100644 --- a/maven/spec/dependabot/maven/version_spec.rb +++ b/maven/spec/dependabot/maven/version_spec.rb @@ -29,24 +29,6 @@ it { is_expected.to be(true) } end - - context "with a nil version" do - let(:version_string) { nil } - - it { is_expected.to be(false) } - end - - context "with an empty version" do - let(:version_string) { "" } - - it { is_expected.to be(false) } - end - - context "with a malformed version string" do - let(:version_string) { "-" } - - it { is_expected.to be(false) } - end end describe "#to_s" do @@ -87,33 +69,6 @@ it { is_expected.to eq("1.0.0_pre1") } end - - context "with a nil version" do - let(:version_string) { nil } - let(:err_msg) { "Malformed version string - string is nil" } - - it "raises an exception" do - expect { version }.to raise_error(Dependabot::BadRequirementError, err_msg) - end - end - - context "with an empty version" do - let(:version_string) { "" } - let(:err_msg) { "Malformed version string - string is empty" } - - it "raises an exception" do - expect { version }.to raise_error(Dependabot::BadRequirementError, err_msg) - end - end - - context "with a malformed version string" do - let(:version_string) { "-" } - let(:err_msg) { "Malformed version string - #{version_string}" } - - it "raises an exception" do - expect { version }.to raise_error(Dependabot::BadRequirementError, err_msg) - end - end end describe "#prerelease?" do @@ -160,22 +115,24 @@ it { is_expected.to be(false) } end - end - describe "#inspect" do - subject { described_class.new(version_string).inspect } + context "with a 'pr' pre-release separated with a ." do + let(:version_string) { "2.10.0.pr3" } - let(:version_string) { "1.0.0+build1" } + it { is_expected.to be(true) } + end - it { is_expected.to eq("#<#{described_class} #{version_string}>") } - end + context "with a 'pre' pre-release separated with a -" do + let(:version_string) { "2.10.0-pre0" } - describe "#to_semver" do - subject { described_class.new(version_string).to_semver } + it { is_expected.to be(true) } + end - let(:version_string) { "1.0.0+build1" } + context "with a dev token" do + let(:version_string) { "1.2.1-dev-65" } - it { is_expected.to eq version_string } + it { is_expected.to be(true) } + end end describe "#<=>" do @@ -201,336 +158,227 @@ end end - context "with semantic versions" do - let(:versions) do - [ - ["1.2.3", "1.2.2", 1], - ["1.2.3", "1.2.3", 0], - ["1.2.3", "v1.2.3", 0] - ] + context "when comparing to a Maven::Version" do + context "when lower" do + let(:other_version) { described_class.new("0.9.0") } + + it { is_expected.to eq(1) } end - it "returns the correct result" do - versions.each do |input| - version1, version2, result = input - version = described_class.new(version1) - other_version = described_class.new(version2) - expect(version <=> other_version).to eq(result) - expect(other_version <=> version).to eq(-result) + context "when equal" do + let(:other_version) { described_class.new("1.0.0") } + + it { is_expected.to eq(0) } + + context "when prefixed with a v" do + let(:other_version) { described_class.new("v1.0.0") } + + it { is_expected.to eq(0) } end - end - end - context "with semantic versions that have a build number" do - let(:versions) do - [ - ["1.2.3", "1.2.3-1", -1], - ["1.2.3", "1.2.3-0-1", -1], - ["1.2.3", "1.2.3-0.1", -1], - ["1.2.3-2", "1.2.3-1", 1], - ["1.2.3-0.2", "1.2.3-1.1", -1] - ] - end + context "when using different date formats" do + let(:version_string) { "20181003" } + let(:other_version) { described_class.new("v2018-10-03") } - it "returns the correct result" do - versions.each do |input| - version1, version2, result = input - version = described_class.new(version1) - other_version = described_class.new(version2) - expect(version <=> other_version).to eq(result) - expect(other_version <=> version).to eq(-result) + it { is_expected.to eq(0) } end end - end - context "with semantic versions that have a qualifier" do - let(:versions) do - [ - ["1.2.3", "1.2.3-a0", 1], # alpha has lower precedence - ["1.2.3", "1.2.3-a", -1] # 'a' without a following int is not alpha + context "when greater" do + let(:other_version) { described_class.new("1.1.0") } - ] + it { is_expected.to eq(-1) } end - it "returns the correct result" do - versions.each do |input| - version1, version2, result = input - version = described_class.new(version1) - other_version = described_class.new(version2) - expect(version <=> other_version).to eq(result) - expect(other_version <=> version).to eq(-result) - end - end - end + context "when the version is a post-release" do + let(:other_version) { described_class.new("1.0.0u1") } - context "with versions that have trailing nulls" do - let(:versions) do - [ - ["1alpha-0", "1alpha", 0], - ["1alpha-0", "1alpha0", 0], - ["1alpha-0", "1alpha.0", 0], - ["1alpha-0", "1alpha.z", -1], - - ["1beta-0", "1beta", 0], - ["1beta-0", "1beta0", 0], - ["1beta-0", "1beta.0", 0], - ["1beta-0", "1beta.z", -1], - - ["1rc-0", "1rc0", 0], - ["1rc-0", "1rc", 0], - ["1rc-0", "1rc.0", 0], - ["1rc-0", "1rc.z", -1], - - ["1sp-0", "1sp0", 0], - ["1sp-0", "1sp", 0], - ["1sp-0", "1sp.0", 0], - ["1sp-0", "1sp.z", -1], - - ["1.ga", "1-ga", 0], - ["1.0", "1.ga", 0], - ["1.0.FINAL", "1", 0], - ["1.0", "1.release", 0], - - ["1.2.0", "1.2", 0], - ["1.2.3", "1.2.3-0", 0], - ["1.2.3-0", "1.2.3-0", 0], - ["1.2.3-0", "1.2.3-a0", 1], - ["1.2.3-0", "1.2.3-a", -1], - ["1.2.3-0", "1.2.3-1", -1], - ["1.2.3-0", "1.2.3-0-1", -1], - - ["1snapshot-0", "1snapshot0", 0], - ["1snapshot-0", "1snapshot", 0], - ["1snapshot-0", "1snapshot.0", 0], - ["1snapshot-0", "1snapshot.z", -1], - - ["1milestone-0", "1milestone", 0], - ["1milestone-0", "1milestone0", 0], - ["1milestone-0", "1milestone.0", 0], - ["1milestone-0", "1milestone.z", -1] - ] + it { is_expected.to eq(-1) } end - it "returns the correct result" do - versions.each do |input| - version1, version2, result = input - version = described_class.new(version1) - other_version = described_class.new(version2) - expect(version <=> other_version).to eq(result) - expect(other_version <=> version).to eq(-result) - end + context "when the version is a pre-release" do + let(:other_version) { described_class.new("1.0.0a1") } + + it { is_expected.to eq(1) } end - end - context "with equivalent shortened qualifiers" do - let(:versions) do - [ - ["1alpha-0", "1a0", 0], - ["1beta-0", "1b0", 0], - ["1milestone-0", "1m0", 0] - ] + context "when the version is non-numeric" do + let(:version) { described_class.new("Finchley") } + let(:other_version) { described_class.new("Edgware") } + + it { is_expected.to eq(1) } end - it "returns 0" do - versions.each do |input| - version1, version2, result = input - version = described_class.new(version1) - other_version = described_class.new(version2) - expect(version <=> other_version).to eq(result) - expect(other_version <=> version).to eq(-result) + describe "with a + separated alphanumeric build identifier" do + context "when equal" do + let(:version_string) { "9.0.0+100" } + let(:other_version) { described_class.new("9.0.0+100") } + + it { is_expected.to eq(0) } end - end - end - context "with dot, hyphen and digit / qualifier transitions as separators" do - let(:versions) do - [ - ["1alpha.z", "1alpha-z", 0], - ["1alpha1", "1alpha-1", 0], - ["1alpha-1", "1alpha.1", -1], - ["1beta.z", "1beta-z", 0], - ["1beta1", "1beta-1", 0], - ["1beta-1", "1beta.1", -1], - ["1-a", "1a", 0], - ["1-a", "1.a", 0], - ["1-b", "1-b-1", -1], - ["1-b-1", "1-b.1", -1], - ["1sp.z", "1sp-z", 0], - ["1sp1", "1sp-1", 0], - ["1sp-1", "1sp.1", -1], - ["1rc.z", "1rc-z", 0], - ["1rc1", "1rc-1", 0], - ["1rc-1", "1rc.1", -1], - ["1milestone.z", "1milestone-z", 0], - ["1milestone1", "1milestone-1", 0], - ["1milestone-1", "1milestone.1", -1], - ["1snapshot.z", "1snapshot-z", 0], - ["1snapshot1", "1snapshot-1", 0], - ["1snapshot-1", "1snapshot.1", -1] - ] - end + context "when greater" do + let(:version_string) { "9.0.0+102" } + let(:other_version) { described_class.new("9.0.0+101") } - it "returns 0" do - versions.each do |input| - version1, version2, result = input - version = described_class.new(version1) - other_version = described_class.new(version2) - expect(version <=> other_version).to eq(result) - expect(other_version <=> version).to eq(-result) + it { is_expected.to eq(1) } end - end - end - context "with qualifiers with different precedence" do - let(:versions) do - [ - ["1alpha.1", "1beta.1", -1], - ["1beta.1", "1milestone.1", -1], - ["1milestone.1", "1rc.1", -1], - ["1rc.1", "1snapshot.1", -1], - ["1.sp", "1.ga", 1], - ["1.release", "1.ga", 0] - ] - end + context "when less than" do + let(:version_string) { "9.0.0+100" } + let(:other_version) { described_class.new("9.0.0+101") } - it "returns the correct value" do - versions.each do |input| - version1, version2, result = input - version = described_class.new(version1) - other_version = described_class.new(version2) - expect(version <=> other_version).to eq(result) - expect(other_version <=> version).to eq(-result) + it { is_expected.to eq(-1) } end end - end - context "with equivalent qualifiers cr and rc" do - let(:version) { described_class.new("1.0rc-1") } - let(:other_version) { described_class.new("1.0-cr1") } + describe "from the spec" do + context "when dealing with number padding" do + let(:version) { described_class.new("1") } + let(:other_version) { described_class.new("1.1") } - it "returns 0" do - expect(version <=> other_version).to eq(0) - expect(other_version <=> version).to eq(0) - end - end + it { is_expected.to eq(-1) } + end - context "when comparing alphanumerically" do - let(:versions) do - [ - ["1alpha-z", "1alpha1", -1], - ["1beta-z", "1beta1", -1], - ["1milestone-z", "1milestone1", -1], - ["1rc-z", "1rc1", -1], - ["1snapshot-z", "1snapshot1", -1], - ["1sp-z", "1sp1", -1], - ["181", "DEV", 1] - ] - end + context "when dealing with qualifier padding" do + let(:version) { described_class.new("1-snapshot") } + let(:other_version) { described_class.new("1") } - it "gives higher precedence to digits" do - versions.each do |input| - version1, version2, result = input - version = described_class.new(version1) - other_version = described_class.new(version2) - expect(version <=> other_version).to eq(result) - expect(other_version <=> version).to eq(-result) + it { is_expected.to eq(-1) } end - end - end - context "when comparing alphabetically" do - let(:versions) do - [ - ["1-a", "1-b", -1], - ["Finchley", "Edgware", 1], - ["1.something", "1.SOMETHING", 0] - ] - end + context "when dealing with qualifier padding 1" do + let(:version) { described_class.new("1") } + let(:other_version) { described_class.new("1-sp") } - it "returns the correct result" do - versions.each do |input| - version1, version2, result = input - version = described_class.new(version1) - other_version = described_class.new(version2) - expect(version <=> other_version).to eq(result) - expect(other_version <=> version).to eq(-result) + it { is_expected.to eq(-1) } end - end - end - context "when comparing numerically" do - let(:versions) do - [ - ["1-b.1", "1-b.2", -1], - ["9.0.0+102", "9.0.0+91", 1], - ["1-foo2", "1-foo10", -1] + context "when dealing with switching" do + let(:version) { described_class.new("1-foo2") } + let(:other_version) { described_class.new("1-foo10") } - ] - end + it { is_expected.to eq(-1) } + end - it "returns the correct result" do - versions.each do |input| - version1, version2, result = input - version = described_class.new(version1) - other_version = described_class.new(version2) - expect(version <=> other_version).to eq(result) - expect(other_version <=> version).to eq(-result) + context "when dealing with prefixes" do + let(:version) { described_class.new("1.foo") } + let(:other_version) { described_class.new("1-foo") } + + it { is_expected.to eq(-1) } end - end - end - context "when comparing padded versions" do - let(:versions) do - [ - ["1snapshot.1", "1", -1], - ["1-snapshot", "1", -1], - ["1", "1sp0", -1], - ["1sp.1", "1-a", -1], - ["1", "1.1", -1], - ["1", "1-sp", -1], - ["1-ga-1", "1-1", -1] - ] - end + context "when dealing with prefixes2" do + let(:version) { described_class.new("1-foo") } + let(:other_version) { described_class.new("1-1") } - it "returns the correct result" do - versions.each do |input| - version1, version2, result = input - version = described_class.new(version1) - other_version = described_class.new(version2) - expect(version <=> other_version).to eq(result) - expect(other_version <=> version).to eq(-result) + it { is_expected.to eq(-1) } end - end - end - context "when ordering versions" do - let(:versions) do - [ - described_class.new("NotAVersionSting"), - described_class.new("1.0-alpha"), - described_class.new("1.0a1-SNAPSHOT"), - described_class.new("1.0-alpha1"), - described_class.new("1.0beta1-SNAPSHOT"), - described_class.new("1.0-b2"), - described_class.new("1.0-beta3.SNAPSHOT"), - described_class.new("1.0-beta3"), - described_class.new("1.0-milestone1-SNAPSHOT"), - described_class.new("1.0-m2"), - described_class.new("1.0-rc1-SNAPSHOT"), - described_class.new("1.0-cr1"), - described_class.new("1.0-SNAPSHOT"), - described_class.new("1.0-RELEASE"), - described_class.new("1.0-sp"), - described_class.new("1.0-a"), - described_class.new("1.0-whatever"), - described_class.new("1.0.z"), - described_class.new("1.0.1"), - described_class.new("1.0.1.0.0.0.0.0.0.0.0.0.0.0.1") - ] - end + context "when dealing with prefixes3" do + let(:version) { described_class.new("1-1") } + let(:other_version) { described_class.new("1.1") } + + it { is_expected.to eq(-1) } + end + + context "when dealing with null values" do + let(:version) { described_class.new("1.ga") } + let(:other_version) { described_class.new("1-ga") } + + it { is_expected.to eq(0) } + end + + context "when dealing with null values 2" do + let(:version) { described_class.new("1-ga") } + let(:other_version) { described_class.new("1-0") } + + it { is_expected.to eq(0) } + end + + context "when dealing with null values 3" do + let(:version) { described_class.new("1-0") } + let(:other_version) { described_class.new("1.0") } + + it { is_expected.to eq(0) } + end + + context "when dealing with null values 4" do + let(:version) { described_class.new("1.0") } + let(:other_version) { described_class.new("1") } - it "sorts versions correctly" do - expect(versions.shuffle.sort).to eq(versions) + it { is_expected.to eq(0) } + end + + context "when dealing with null values 5" do + let(:version) { described_class.new("1.0.") } + let(:other_version) { described_class.new("1") } + + it { is_expected.to eq(0) } + end + + context "when dealing with null values 6" do + let(:version) { described_class.new("1.0-.2") } + let(:other_version) { described_class.new("1.0-0.2") } + + it { is_expected.to eq(0) } + end + + context "when dealing with case insensitivity" do + let(:version) { described_class.new("1.0.FINAL") } + let(:other_version) { described_class.new("1") } + + it { is_expected.to eq(0) } + end + + context "when dealing with case insensitivity 2" do + let(:version) { described_class.new("1.something") } + let(:other_version) { described_class.new("1.SOMETHING") } + + it { is_expected.to eq(0) } + end + + context "when dealing with post releases" do + let(:version) { described_class.new("1-sp") } + let(:other_version) { described_class.new("1-ga") } + + it { is_expected.to eq(1) } + end + + context "when dealing with post releases 2" do + let(:version) { described_class.new("1-sp.1") } + let(:other_version) { described_class.new("1-ga.1") } + + it { is_expected.to eq(1) } + end + + context "when dealing with null values (again)" do + let(:version) { described_class.new("1-sp-1") } + let(:other_version) { described_class.new("1-ga-1") } + + it { is_expected.to eq(-1) } + end + + context "when dealing with null values (again 2)" do + let(:version) { described_class.new("1-ga-1") } + let(:other_version) { described_class.new("1-1") } + + it { is_expected.to eq(0) } + end + + context "when dealing with named values" do + let(:version) { described_class.new("1-a1") } + let(:other_version) { described_class.new("1-alpha-1") } + + it { is_expected.to eq(0) } + end + + context "when comparing string versions with integer ones" do + let(:version) { described_class.new("181") } + let(:other_version) { described_class.new("dev") } + + it { is_expected.to eq(1) } + end end end end