summaryrefslogtreecommitdiff
path: root/cpan/Version-Requirements
diff options
context:
space:
mode:
authorDavid Golden <dagolden@cpan.org>2011-02-11 09:31:03 -0500
committerDavid Golden <dagolden@cpan.org>2011-02-11 11:04:04 -0500
commit39ac3336da840fc340d83005a1b728db40a469ef (patch)
tree1fe133d4b7e9267e2f2912da785e9cfff6d9c6bd /cpan/Version-Requirements
parentd7bdb612ac7ecb7adfb85cf8f7484e81501984a7 (diff)
downloadperl-39ac3336da840fc340d83005a1b728db40a469ef.tar.gz
Add Version::Requirements as a dual-life module
Version::Requirements version 0.101020 has been added as a dual-life module. It provides a standard library to model and manipulates module prerequisites and version constraints as defined in the CPAN::Meta::Spec. It is a prerequisite for CPAN::Meta, which will be used by the Perl CPAN Toolchain to standardize interactions with META and MYMETA files.
Diffstat (limited to 'cpan/Version-Requirements')
-rw-r--r--cpan/Version-Requirements/Changes28
-rw-r--r--cpan/Version-Requirements/lib/Version/Requirements.pm596
-rw-r--r--cpan/Version-Requirements/t/accepts.t29
-rw-r--r--cpan/Version-Requirements/t/basic.t224
-rw-r--r--cpan/Version-Requirements/t/finalize.t91
-rw-r--r--cpan/Version-Requirements/t/from-hash.t48
-rw-r--r--cpan/Version-Requirements/t/merge.t136
7 files changed, 1152 insertions, 0 deletions
diff --git a/cpan/Version-Requirements/Changes b/cpan/Version-Requirements/Changes
new file mode 100644
index 0000000000..d3217bb049
--- /dev/null
+++ b/cpan/Version-Requirements/Changes
@@ -0,0 +1,28 @@
+Revision history for Version-Requirements
+
+0.101020 2010-04-12 09:08:26 America/New_York
+ add finalization with ->finalize and ->is_finalized
+
+0.101010 2010-04-11 10:20:02 America/New_York
+ add a public accepts_module version
+
+0.100660 2010-03-07 13:05:59 America/New_York
+ specify that we need version.pm 0.77
+
+0.100630 2010-03-04 22:54:15 America/New_York
+ do not lose ">= 0" requirements on merge
+
+0.100530 2010-02-22 21:09:40 America/New_York
+ add is_simple method to V:R
+ as_string_hash will now simplify ">= 1, != 1" to "> 1"
+
+0.100520 2010-02-21 23:15:34 America/New_York
+ add_requirements method added (to merge V:R objects)
+ clear_requirement method added
+ from_hash_string method added
+ clone method added
+ required_modules method added
+
+0.100510 2010-02-20 17:27:19 America/New_York
+ first release!
+
diff --git a/cpan/Version-Requirements/lib/Version/Requirements.pm b/cpan/Version-Requirements/lib/Version/Requirements.pm
new file mode 100644
index 0000000000..f667189282
--- /dev/null
+++ b/cpan/Version-Requirements/lib/Version/Requirements.pm
@@ -0,0 +1,596 @@
+use strict;
+use warnings;
+package Version::Requirements;
+BEGIN {
+ $Version::Requirements::VERSION = '0.101020';
+}
+# ABSTRACT: a set of version requirements for a CPAN dist
+
+
+use Carp ();
+use Scalar::Util ();
+use version 0.77 (); # the ->parse method
+
+
+sub new {
+ my ($class) = @_;
+ return bless {} => $class;
+}
+
+sub _version_object {
+ my ($self, $version) = @_;
+
+ $version = (! defined $version) ? version->parse(0)
+ : (! Scalar::Util::blessed($version)) ? version->parse($version)
+ : $version;
+
+ return $version;
+}
+
+
+BEGIN {
+ for my $type (qw(minimum maximum exclusion exact_version)) {
+ my $method = "with_$type";
+ my $to_add = $type eq 'exact_version' ? $type : "add_$type";
+
+ my $code = sub {
+ my ($self, $name, $version) = @_;
+
+ $version = $self->_version_object( $version );
+
+ $self->__modify_entry_for($name, $method, $version);
+
+ return $self;
+ };
+
+ no strict 'refs';
+ *$to_add = $code;
+ }
+}
+
+
+sub add_requirements {
+ my ($self, $req) = @_;
+
+ for my $module ($req->required_modules) {
+ my $modifiers = $req->__entry_for($module)->as_modifiers;
+ for my $modifier (@$modifiers) {
+ my ($method, @args) = @$modifier;
+ $self->$method($module => @args);
+ };
+ }
+
+ return $self;
+}
+
+
+sub accepts_module {
+ my ($self, $module, $version) = @_;
+
+ $version = $self->_version_object( $version );
+
+ return 1 unless my $range = $self->__entry_for($module);
+ return $range->_accepts($version);
+}
+
+
+sub clear_requirement {
+ my ($self, $module) = @_;
+
+ return $self unless $self->__entry_for($module);
+
+ Carp::confess("can't clear requirements on finalized requirements")
+ if $self->is_finalized;
+
+ delete $self->{requirements}{ $module };
+
+ return $self;
+}
+
+
+sub required_modules { keys %{ $_[0]{requirements} } }
+
+
+sub clone {
+ my ($self) = @_;
+ my $new = (ref $self)->new;
+
+ return $new->add_requirements($self);
+}
+
+sub __entry_for { $_[0]{requirements}{ $_[1] } }
+
+sub __modify_entry_for {
+ my ($self, $name, $method, $version) = @_;
+
+ my $fin = $self->is_finalized;
+ my $old = $self->__entry_for($name);
+
+ Carp::confess("can't add new requirements to finalized requirements")
+ if $fin and not $old;
+
+ my $new = ($old || 'Version::Requirements::_Range::Range')
+ ->$method($version);
+
+ Carp::confess("can't modify finalized requirements")
+ if $fin and $old->as_string ne $new->as_string;
+
+ $self->{requirements}{ $name } = $new;
+}
+
+
+sub is_simple {
+ my ($self) = @_;
+ for my $module ($self->required_modules) {
+ # XXX: This is a complete hack, but also entirely correct.
+ return if $self->__entry_for($module)->as_string =~ /\s/;
+ }
+
+ return 1;
+}
+
+
+sub is_finalized { $_[0]{finalized} }
+
+
+sub finalize { $_[0]{finalized} = 1 }
+
+
+sub as_string_hash {
+ my ($self) = @_;
+
+ my %hash = map {; $_ => $self->{requirements}{$_}->as_string }
+ $self->required_modules;
+
+ return \%hash;
+}
+
+
+my %methods_for_op = (
+ '==' => [ qw(exact_version) ],
+ '!=' => [ qw(add_exclusion) ],
+ '>=' => [ qw(add_minimum) ],
+ '<=' => [ qw(add_maximum) ],
+ '>' => [ qw(add_minimum add_exclusion) ],
+ '<' => [ qw(add_maximum add_exclusion) ],
+);
+
+sub from_string_hash {
+ my ($class, $hash) = @_;
+
+ my $self = $class->new;
+
+ for my $module (keys %$hash) {
+ my @parts = split qr{\s*,\s*}, $hash->{ $module };
+ for my $part (@parts) {
+ my ($op, $ver) = split /\s+/, $part, 2;
+
+ if (! defined $ver) {
+ $self->add_minimum($module => $op);
+ } else {
+ Carp::confess("illegal requirement string: $hash->{ $module }")
+ unless my $methods = $methods_for_op{ $op };
+
+ $self->$_($module => $ver) for @$methods;
+ }
+ }
+ }
+
+ return $self;
+}
+
+##############################################################
+
+{
+ package
+ Version::Requirements::_Range::Exact;
+BEGIN {
+ $Version::Requirements::_Range::Exact::VERSION = '0.101020';
+}
+ sub _new { bless { version => $_[1] } => $_[0] }
+
+ sub _accepts { return $_[0]{version} == $_[1] }
+
+ sub as_string { return "== $_[0]{version}" }
+
+ sub as_modifiers { return [ [ exact_version => $_[0]{version} ] ] }
+
+ sub _clone {
+ (ref $_[0])->_new( version->new( $_[0]{version} ) )
+ }
+
+ sub with_exact_version {
+ my ($self, $version) = @_;
+
+ return $self->_clone if $self->_accepts($version);
+
+ Carp::confess("illegal requirements: unequal exact version specified");
+ }
+
+ sub with_minimum {
+ my ($self, $minimum) = @_;
+ return $self->_clone if $self->{version} >= $minimum;
+ Carp::confess("illegal requirements: minimum above exact specification");
+ }
+
+ sub with_maximum {
+ my ($self, $maximum) = @_;
+ return $self->_clone if $self->{version} <= $maximum;
+ Carp::confess("illegal requirements: maximum below exact specification");
+ }
+
+ sub with_exclusion {
+ my ($self, $exclusion) = @_;
+ return $self->_clone unless $exclusion == $self->{version};
+ Carp::confess("illegal requirements: excluded exact specification");
+ }
+}
+
+##############################################################
+
+{
+ package
+ Version::Requirements::_Range::Range;
+BEGIN {
+ $Version::Requirements::_Range::Range::VERSION = '0.101020';
+}
+
+ sub _self { ref($_[0]) ? $_[0] : (bless { } => $_[0]) }
+
+ sub _clone {
+ return (bless { } => $_[0]) unless ref $_[0];
+
+ my ($s) = @_;
+ my %guts = (
+ (exists $s->{minimum} ? (minimum => version->new($s->{minimum})) : ()),
+ (exists $s->{maximum} ? (maximum => version->new($s->{maximum})) : ()),
+
+ (exists $s->{exclusions}
+ ? (exclusions => [ map { version->new($_) } @{ $s->{exclusions} } ])
+ : ()),
+ );
+
+ bless \%guts => ref($s);
+ }
+
+ sub as_modifiers {
+ my ($self) = @_;
+ my @mods;
+ push @mods, [ add_minimum => $self->{minimum} ] if exists $self->{minimum};
+ push @mods, [ add_maximum => $self->{maximum} ] if exists $self->{maximum};
+ push @mods, map {; [ add_exclusion => $_ ] } @{$self->{exclusions} || []};
+ return \@mods;
+ }
+
+ sub as_string {
+ my ($self) = @_;
+
+ return 0 if ! keys %$self;
+
+ return "$self->{minimum}" if (keys %$self) == 1 and exists $self->{minimum};
+
+ my @exclusions = @{ $self->{exclusions} || [] };
+
+ my @parts;
+
+ for my $pair (
+ [ qw( >= > minimum ) ],
+ [ qw( <= < maximum ) ],
+ ) {
+ my ($op, $e_op, $k) = @$pair;
+ if (exists $self->{$k}) {
+ my @new_exclusions = grep { $_ != $self->{ $k } } @exclusions;
+ if (@new_exclusions == @exclusions) {
+ push @parts, "$op $self->{ $k }";
+ } else {
+ push @parts, "$e_op $self->{ $k }";
+ @exclusions = @new_exclusions;
+ }
+ }
+ }
+
+ push @parts, map {; "!= $_" } @exclusions;
+
+ return join q{, }, @parts;
+ }
+
+ sub with_exact_version {
+ my ($self, $version) = @_;
+ $self = $self->_clone;
+
+ Carp::confess("illegal requirements: exact specification outside of range")
+ unless $self->_accepts($version);
+
+ return Version::Requirements::_Range::Exact->_new($version);
+ }
+
+ sub _simplify {
+ my ($self) = @_;
+
+ if (defined $self->{minimum} and defined $self->{maximum}) {
+ if ($self->{minimum} == $self->{maximum}) {
+ Carp::confess("illegal requirements: excluded all values")
+ if grep { $_ == $self->{minimum} } @{ $self->{exclusions} || [] };
+
+ return Version::Requirements::_Range::Exact->_new($self->{minimum})
+ }
+
+ Carp::confess("illegal requirements: minimum exceeds maximum")
+ if $self->{minimum} > $self->{maximum};
+ }
+
+ # eliminate irrelevant exclusions
+ if ($self->{exclusions}) {
+ my %seen;
+ @{ $self->{exclusions} } = grep {
+ (! defined $self->{minimum} or $_ >= $self->{minimum})
+ and
+ (! defined $self->{maximum} or $_ <= $self->{maximum})
+ and
+ ! $seen{$_}++
+ } @{ $self->{exclusions} };
+ }
+
+ return $self;
+ }
+
+ sub with_minimum {
+ my ($self, $minimum) = @_;
+ $self = $self->_clone;
+
+ if (defined (my $old_min = $self->{minimum})) {
+ $self->{minimum} = (sort { $b cmp $a } ($minimum, $old_min))[0];
+ } else {
+ $self->{minimum} = $minimum;
+ }
+
+ return $self->_simplify;
+ }
+
+ sub with_maximum {
+ my ($self, $maximum) = @_;
+ $self = $self->_clone;
+
+ if (defined (my $old_max = $self->{maximum})) {
+ $self->{maximum} = (sort { $a cmp $b } ($maximum, $old_max))[0];
+ } else {
+ $self->{maximum} = $maximum;
+ }
+
+ return $self->_simplify;
+ }
+
+ sub with_exclusion {
+ my ($self, $exclusion) = @_;
+ $self = $self->_clone;
+
+ push @{ $self->{exclusions} ||= [] }, $exclusion;
+
+ return $self->_simplify;
+ }
+
+ sub _accepts {
+ my ($self, $version) = @_;
+
+ return if defined $self->{minimum} and $version < $self->{minimum};
+ return if defined $self->{maximum} and $version > $self->{maximum};
+ return if defined $self->{exclusions}
+ and grep { $version == $_ } @{ $self->{exclusions} };
+
+ return 1;
+ }
+}
+
+1;
+
+__END__
+=pod
+
+=head1 NAME
+
+Version::Requirements - a set of version requirements for a CPAN dist
+
+=head1 VERSION
+
+version 0.101020
+
+=head1 SYNOPSIS
+
+ use Version::Requirements;
+
+ my $build_requires = Version::Requirements->new;
+
+ $build_requires->add_minimum('Library::Foo' => 1.208);
+
+ $build_requires->add_minimum('Library::Foo' => 2.602);
+
+ $build_requires->add_minimum('Module::Bar' => 'v1.2.3');
+
+ $METAyml->{build_requires} = $build_requires->as_string_hash;
+
+=head1 DESCRIPTION
+
+A Version::Requirements object models a set of version constraints like those
+specified in the F<META.yml> or F<META.json> files in CPAN distributions. It
+can be built up by adding more and more constraints, and it will reduce them to
+the simplest representation.
+
+Logically impossible constraints will be identified immediately by thrown
+exceptions.
+
+=head1 METHODS
+
+=head2 new
+
+ my $req = Version::Requirements->new;
+
+This returns a new Version::Requirements object. It ignores any arguments
+given.
+
+=head2 add_minimum
+
+ $req->add_minimum( $module => $version );
+
+This adds a new minimum version requirement. If the new requirement is
+redundant to the existing specification, this has no effect.
+
+Minimum requirements are inclusive. C<$version> is required, along with any
+greater version number.
+
+This method returns the requirements object.
+
+=head2 add_maximum
+
+ $req->add_maximum( $module => $version );
+
+This adds a new maximum version requirement. If the new requirement is
+redundant to the existing specification, this has no effect.
+
+Maximum requirements are inclusive. No version strictly greater than the given
+version is allowed.
+
+This method returns the requirements object.
+
+=head2 add_exclusion
+
+ $req->add_exclusion( $module => $version );
+
+This adds a new excluded version. For example, you might use these three
+method calls:
+
+ $req->add_minimum( $module => '1.00' );
+ $req->add_maximum( $module => '1.82' );
+
+ $req->add_exclusion( $module => '1.75' );
+
+Any version between 1.00 and 1.82 inclusive would be acceptable, except for
+1.75.
+
+This method returns the requirements object.
+
+=head2 exact_version
+
+ $req->exact_version( $module => $version );
+
+This sets the version required for the given module to I<exactly> the given
+version. No other version would be considered acceptable.
+
+This method returns the requirements object.
+
+=head2 add_requirements
+
+ $req->add_requirements( $another_req_object );
+
+This method adds all the requirements in the given Version::Requirements object
+to the requirements object on which it was called. If there are any conflicts,
+an exception is thrown.
+
+This method returns the requirements object.
+
+=head2 accepts_module
+
+ my $bool = $req->accepts_modules($module => $version);
+
+Given an module and version, this method returns true if the version
+specification for the module accepts the provided version. In other words,
+given:
+
+ Module => '>= 1.00, < 2.00'
+
+We will accept 1.00 and 1.75 but not 0.50 or 2.00.
+
+For modules that do not appear in the requirements, this method will return
+true.
+
+=head2 clear_requirement
+
+ $req->clear_requirement( $module );
+
+This removes the requirement for a given module from the object.
+
+This method returns the requirements object.
+
+=head2 required_modules
+
+This method returns a list of all the modules for which requirements have been
+specified.
+
+=head2 clone
+
+ $req->clone;
+
+This method returns a clone of the invocant. The clone and the original object
+can then be changed independent of one another.
+
+=head2 is_simple
+
+This method returns true if and only if all requirements are inclusive minimums
+-- that is, if their string expression is just the version number.
+
+=head2 is_finalized
+
+This method returns true if the requirements have been finalized by having the
+C<finalize> method called on them.
+
+=head2 finalize
+
+This method marks the requirements finalized. Subsequent attempts to change
+the requirements will be fatal, I<if> they would result in a change. If they
+would not alter the requirements, they have no effect.
+
+If a finalized set of requirements is cloned, the cloned requirements are not
+also finalized.
+
+=head2 as_string_hash
+
+This returns a reference to a hash describing the requirements using the
+strings in the F<META.yml> specification.
+
+For example after the following program:
+
+ my $req = Version::Requirements->new;
+
+ $req->add_minimum('Version::Requirements' => 0.102);
+
+ $req->add_minimum('Library::Foo' => 1.208);
+
+ $req->add_maximum('Library::Foo' => 2.602);
+
+ $req->add_minimum('Module::Bar' => 'v1.2.3');
+
+ $req->add_exclusion('Module::Bar' => 'v1.2.8');
+
+ $req->exact_version('Xyzzy' => '6.01');
+
+ my $hashref = $req->as_string_hash;
+
+C<$hashref> would contain:
+
+ {
+ 'Version::Requirements' => '0.102',
+ 'Library::Foo' => '>= 1.208, <= 2.206',
+ 'Module::Bar' => '>= v1.2.3, != v1.2.8',
+ 'Xyzzy' => '== 6.01',
+ }
+
+=head2 from_string_hash
+
+ my $req = Version::Requirements->from_string_hash( \%hash );
+
+This is an alternate constructor for a Version::Requirements object. It takes
+a hash of module names and version requirement strings and returns a new
+Version::Requirements object.
+
+=head1 AUTHOR
+
+ Ricardo Signes <rjbs@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2010 by Ricardo Signes.
+
+This is free software; you can redistribute it and/or modify it under
+the same terms as the Perl 5 programming language system itself.
+
+=cut
+
diff --git a/cpan/Version-Requirements/t/accepts.t b/cpan/Version-Requirements/t/accepts.t
new file mode 100644
index 0000000000..5c00c5575c
--- /dev/null
+++ b/cpan/Version-Requirements/t/accepts.t
@@ -0,0 +1,29 @@
+use strict;
+use warnings;
+
+use Version::Requirements;
+
+use Test::More 0.88;
+
+{
+ my $req = Version::Requirements->new->add_minimum(Foo => 1);
+
+ ok( $req->accepts_module(Foo => 1));
+ ok(! $req->accepts_module(Foo => 0));
+}
+
+{
+ my $req = Version::Requirements->new->add_maximum(Foo => 1);
+
+ ok( $req->accepts_module(Foo => 1));
+ ok(! $req->accepts_module(Foo => 2));
+}
+
+{
+ my $req = Version::Requirements->new->add_exclusion(Foo => 1);
+
+ ok( $req->accepts_module(Foo => 0));
+ ok(! $req->accepts_module(Foo => 1));
+}
+
+done_testing;
diff --git a/cpan/Version-Requirements/t/basic.t b/cpan/Version-Requirements/t/basic.t
new file mode 100644
index 0000000000..27fb6ccac3
--- /dev/null
+++ b/cpan/Version-Requirements/t/basic.t
@@ -0,0 +1,224 @@
+use strict;
+use warnings;
+
+use Version::Requirements;
+
+use Test::More 0.88;
+
+sub dies_ok (&@) {
+ my ($code, $qr, $comment) = @_;
+
+ my $lived = eval { $code->(); 1 };
+
+ if ($lived) {
+ fail("$comment: did not die");
+ } else {
+ like($@, $qr, $comment);
+ }
+}
+
+{
+ my $req = Version::Requirements->new;
+
+ $req->add_minimum('Foo::Bar' => 10);
+ $req->add_minimum('Foo::Bar' => 0);
+ $req->add_minimum('Foo::Bar' => 2);
+
+ $req->add_minimum('Foo::Baz' => version->declare('v1.2.3'));
+
+ $req->add_minimum('Foo::Undef' => undef);
+
+ is_deeply(
+ $req->as_string_hash,
+ {
+ 'Foo::Bar' => 10,
+ 'Foo::Baz' => 'v1.2.3',
+ 'Foo::Undef' => 0,
+ },
+ "some basic minimums",
+ );
+
+ ok($req->is_simple, "just minimums? simple");
+}
+
+{
+ my $req = Version::Requirements->new;
+ $req->add_maximum(Foo => 1);
+ is_deeply($req->as_string_hash, { Foo => '<= 1' }, "max only");
+
+ ok(! $req->is_simple, "maximums? not simple");
+}
+
+{
+ my $req = Version::Requirements->new;
+ $req->add_exclusion(Foo => 1);
+ $req->add_exclusion(Foo => 2);
+
+ # Why would you ever do this?? -- rjbs, 2010-02-20
+ is_deeply($req->as_string_hash, { Foo => '!= 1, != 2' }, "excl only");
+}
+
+{
+ my $req = Version::Requirements->new;
+
+ $req->add_minimum(Foo => 1);
+ $req->add_maximum(Foo => 2);
+
+ is_deeply(
+ $req->as_string_hash,
+ {
+ Foo => '>= 1, <= 2',
+ },
+ "min and max",
+ );
+
+ $req->add_maximum(Foo => 3);
+
+ is_deeply(
+ $req->as_string_hash,
+ {
+ Foo => '>= 1, <= 2',
+ },
+ "exclusions already outside range do not matter",
+ );
+
+ $req->add_exclusion(Foo => 1.5);
+
+ is_deeply(
+ $req->as_string_hash,
+ {
+ Foo => '>= 1, <= 2, != 1.5',
+ },
+ "exclusions",
+ );
+
+ $req->add_minimum(Foo => 1.6);
+
+ is_deeply(
+ $req->as_string_hash,
+ {
+ Foo => '>= 1.6, <= 2',
+ },
+ "exclusions go away when made irrelevant",
+ );
+}
+
+{
+ my $req = Version::Requirements->new;
+
+ $req->add_minimum(Foo => 1);
+ $req->add_exclusion(Foo => 1);
+ $req->add_maximum(Foo => 2);
+
+ is_deeply(
+ $req->as_string_hash,
+ {
+ Foo => '> 1, <= 2',
+ },
+ "we can exclude an endpoint",
+ );
+}
+
+{
+ my $req = Version::Requirements->new;
+ $req->add_minimum(Foo => 1);
+
+ $req->add_exclusion(Foo => 1);
+
+ dies_ok { $req->add_maximum(Foo => 1); }
+ qr/excluded all/,
+ "can't exclude all values" ;
+}
+
+{
+ my $req = Version::Requirements->new;
+ $req->add_minimum(Foo => 1);
+ dies_ok {$req->exact_version(Foo => 0.5); }
+ qr/outside of range/,
+ "can't add outside-range exact spec to range";
+}
+
+{
+ my $req = Version::Requirements->new;
+ $req->add_minimum(Foo => 1);
+ dies_ok { $req->add_maximum(Foo => 0.5); }
+ qr/minimum exceeds maximum/,
+ "maximum must exceed (or equal) minimum";
+
+ $req = Version::Requirements->new;
+ $req->add_maximum(Foo => 0.5);
+ dies_ok { $req->add_minimum(Foo => 1); }
+ qr/minimum exceeds maximum/,
+ "maximum must exceed (or equal) minimum";
+}
+
+{
+ my $req = Version::Requirements->new;
+
+ $req->add_minimum(Foo => 1);
+ $req->add_maximum(Foo => 1);
+
+ $req->add_maximum(Foo => 2); # ignored
+ $req->add_minimum(Foo => 0); # ignored
+ $req->add_exclusion(Foo => .5); # ignored
+
+ is_deeply(
+ $req->as_string_hash,
+ {
+ 'Foo' => '== 1',
+ },
+ "if min==max, becomes exact requirement",
+ );
+}
+
+{
+ my $req = Version::Requirements->new;
+ $req->add_minimum(Foo => 1);
+ $req->add_exclusion(Foo => 0);
+ $req->add_maximum(Foo => 3);
+ $req->add_exclusion(Foo => 4);
+
+ $req->add_exclusion(Foo => 2);
+ $req->add_exclusion(Foo => 2);
+
+ is_deeply(
+ $req->as_string_hash,
+ {
+ Foo => '>= 1, <= 3, != 2',
+ },
+ 'test exclusion-skipping',
+ );
+}
+
+sub foo_1 {
+ my $req = Version::Requirements->new;
+ $req->exact_version(Foo => 1);
+ return $req;
+}
+
+{
+ my $req = foo_1;
+
+ $req->exact_version(Foo => 1); # ignored
+
+ is_deeply($req->as_string_hash, { Foo => '== 1' }, "exact requirement");
+
+ dies_ok { $req->exact_version(Foo => 2); }
+ qr/unequal/,
+ "can't exactly specify differing versions" ;
+
+ $req = foo_1;
+ $req->add_minimum(Foo => 0); # ignored
+ $req->add_maximum(Foo => 2); # ignored
+
+ dies_ok { $req->add_maximum(Foo => 0); } qr/maximum below/, "max < fixed";
+
+ $req = foo_1;
+ dies_ok { $req->add_minimum(Foo => 2); } qr/minimum above/, "min > fixed";
+
+ $req = foo_1;
+ $req->add_exclusion(Foo => 8); # ignored
+ dies_ok { $req->add_exclusion(Foo => 1); } qr/excluded exact/, "!= && ==";
+}
+
+done_testing;
diff --git a/cpan/Version-Requirements/t/finalize.t b/cpan/Version-Requirements/t/finalize.t
new file mode 100644
index 0000000000..fe9a180d7c
--- /dev/null
+++ b/cpan/Version-Requirements/t/finalize.t
@@ -0,0 +1,91 @@
+use strict;
+use warnings;
+
+use Version::Requirements;
+
+use Test::More 0.88;
+
+sub dies_ok (&@) {
+ my ($code, $qr, $comment) = @_;
+
+ my $lived = eval { $code->(); 1 };
+
+ if ($lived) {
+ fail("$comment: did not die");
+ } else {
+ like($@, $qr, $comment);
+ }
+}
+
+{
+ my $req = Version::Requirements->new;
+
+ $req->add_minimum('Foo::Bar' => 10);
+ $req->add_minimum('Foo::Bar' => 0);
+ $req->add_minimum('Foo::Bar' => 2);
+
+ $req->add_minimum('Foo::Baz' => version->declare('v1.2.3'));
+
+ $req->add_minimum('Foo::Undef' => undef);
+
+ my $want = {
+ 'Foo::Bar' => 10,
+ 'Foo::Baz' => 'v1.2.3',
+ 'Foo::Undef' => 0,
+ };
+
+ is_deeply(
+ $req->as_string_hash,
+ $want,
+ "some basic minimums",
+ );
+
+ $req->finalize;
+
+ $req->add_minimum('Foo::Bar', 2);
+
+ pass('we can add a Foo::Bar requirement with no effect post finalization');
+
+ dies_ok { $req->add_minimum('Foo::Bar', 12) }
+ qr{finalized req},
+ "can't add a higher Foo::Bar after finalization";
+
+ dies_ok { $req->add_minimum('Foo::New', 0) }
+ qr{finalized req},
+ "can't add a new module prereq after finalization";
+
+ dies_ok { $req->clear_requirement('Foo::Bar') }
+ qr{finalized req},
+ "can't clear an existing prereq after finalization";
+
+ $req->clear_requirement('Bogus::Req');
+
+ pass('we can clear a prereq that was not set to begin with');
+
+ is_deeply(
+ $req->as_string_hash,
+ $want,
+ "none of our attempts to alter the object post-finalization worked",
+ );
+
+ my $cloned = $req->clone;
+
+ $cloned->add_minimum('Foo::Bar', 12);
+
+ is_deeply(
+ $cloned->as_string_hash,
+ {
+ %$want,
+ 'Foo::Bar' => 12,
+ },
+ "we can alter a cloned V:R (finalization does not survive cloning)",
+ );
+
+ is_deeply(
+ $req->as_string_hash,
+ $want,
+ "...and original requirements are untouched",
+ );
+}
+
+done_testing;
diff --git a/cpan/Version-Requirements/t/from-hash.t b/cpan/Version-Requirements/t/from-hash.t
new file mode 100644
index 0000000000..5c2bca3f76
--- /dev/null
+++ b/cpan/Version-Requirements/t/from-hash.t
@@ -0,0 +1,48 @@
+use strict;
+use warnings;
+
+use Version::Requirements;
+
+use Test::More 0.88;
+
+sub dies_ok (&@) {
+ my ($code, $qr, $comment) = @_;
+
+ my $lived = eval { $code->(); 1 };
+
+ if ($lived) {
+ fail("$comment: did not die");
+ } else {
+ like($@, $qr, $comment);
+ }
+}
+
+{
+ my $string_hash = {
+ Left => 10,
+ Shared => '>= 2, <= 9, != 7',
+ Right => 18,
+ };
+
+ my $req = Version::Requirements->from_string_hash($string_hash);
+
+ is_deeply(
+ $req->as_string_hash,
+ $string_hash,
+ "we can load from a string hash",
+ );
+}
+
+{
+ my $string_hash = {
+ Left => 10,
+ Shared => '= 2',
+ Right => 18,
+ };
+
+ dies_ok { Version::Requirements->from_string_hash($string_hash) }
+ qr/illegal/,
+ "we die when we can't understand a version spec";
+}
+
+done_testing;
diff --git a/cpan/Version-Requirements/t/merge.t b/cpan/Version-Requirements/t/merge.t
new file mode 100644
index 0000000000..2611d46155
--- /dev/null
+++ b/cpan/Version-Requirements/t/merge.t
@@ -0,0 +1,136 @@
+use strict;
+use warnings;
+
+use Version::Requirements;
+
+use Test::More 0.88;
+
+sub dies_ok (&@) {
+ my ($code, $qr, $comment) = @_;
+
+ my $lived = eval { $code->(); 1 };
+
+ if ($lived) {
+ fail("$comment: did not die");
+ } else {
+ like($@, $qr, $comment);
+ }
+}
+
+{
+ my $req_1 = Version::Requirements->new;
+ $req_1->add_minimum(Left => 10);
+ $req_1->add_minimum(Shared => 2);
+ $req_1->add_exclusion(Shared => 7);
+
+ my $req_2 = Version::Requirements->new;
+ $req_2->add_minimum(Shared => 1);
+ $req_2->add_maximum(Shared => 9);
+ $req_2->add_minimum(Right => 18);
+
+ $req_1->add_requirements($req_2);
+
+ is_deeply(
+ $req_1->as_string_hash,
+ {
+ Left => 10,
+ Shared => '>= 2, <= 9, != 7',
+ Right => 18,
+ },
+ "add requirements to an existing set of requirements",
+ );
+}
+
+{
+ my $req_1 = Version::Requirements->new;
+ $req_1->add_minimum(Left => 10);
+ $req_1->add_minimum(Shared => 2);
+ $req_1->add_exclusion(Shared => 7);
+ $req_1->exact_version(Exact => 8);
+
+ my $req_2 = Version::Requirements->new;
+ $req_2->add_minimum(Shared => 1);
+ $req_2->add_maximum(Shared => 9);
+ $req_2->add_minimum(Right => 18);
+ $req_2->exact_version(Exact => 8);
+
+ my $clone = $req_1->clone->add_requirements($req_2);
+
+ is_deeply(
+ $req_1->as_string_hash,
+ {
+ Left => 10,
+ Shared => '>= 2, != 7',
+ Exact => '== 8',
+ },
+ "clone/add_requirements does not affect lhs",
+ );
+
+ is_deeply(
+ $req_2->as_string_hash,
+ {
+ Shared => '>= 1, <= 9',
+ Right => 18,
+ Exact => '== 8',
+ },
+ "clone/add_requirements does not affect rhs",
+ );
+
+ is_deeply(
+ $clone->as_string_hash,
+ {
+ Left => 10,
+ Shared => '>= 2, <= 9, != 7',
+ Right => 18,
+ Exact => '== 8',
+ },
+ "clone and add_requirements",
+ );
+
+ $clone->clear_requirement('Shared');
+
+ is_deeply(
+ $clone->as_string_hash,
+ {
+ Left => 10,
+ Right => 18,
+ Exact => '== 8',
+ },
+ "cleared the shared requirement",
+ );
+}
+
+{
+ my $req_1 = Version::Requirements->new;
+ $req_1->add_maximum(Foo => 1);
+
+ my $req_2 = $req_1->clone;
+
+ is_deeply(
+ $req_2->as_string_hash,
+ {
+ 'Foo' => '<= 1',
+ },
+ 'clone with only max',
+ );
+}
+
+{
+ my $left = Version::Requirements->new;
+ $left->add_minimum(Foo => 0);
+ $left->add_minimum(Bar => 1);
+
+ my $right = Version::Requirements->new;
+ $right->add_requirements($left);
+
+ is_deeply(
+ $right->as_string_hash,
+ {
+ Foo => 0,
+ Bar => 1,
+ },
+ "we do not lose 0-min reqs on merge",
+ );
+}
+
+done_testing;