Initial Commit

This commit is contained in:
Riley Schneider
2025-12-03 16:38:10 +01:00
parent c5e26bf594
commit b732d8d4b5
17680 changed files with 5977495 additions and 2 deletions

View File

@@ -0,0 +1,328 @@
package Test2::Compare::Array;
use strict;
use warnings;
use base 'Test2::Compare::Base';
our $VERSION = '0.000139';
use Test2::Util::HashBase qw/inref meta ending items order for_each/;
use Carp qw/croak confess/;
use Scalar::Util qw/reftype looks_like_number/;
sub init {
my $self = shift;
if( defined( my $ref = $self->{+INREF}) ) {
croak "Cannot specify both 'inref' and 'items'" if $self->{+ITEMS};
croak "Cannot specify both 'inref' and 'order'" if $self->{+ORDER};
croak "'inref' must be an array reference, got '$ref'" unless reftype($ref) eq 'ARRAY';
my $order = $self->{+ORDER} = [];
my $items = $self->{+ITEMS} = {};
for (my $i = 0; $i < @$ref; $i++) {
push @$order => $i;
$items->{$i} = $ref->[$i];
}
}
else {
$self->{+ITEMS} ||= {};
croak "All indexes listed in the 'items' hashref must be numeric"
if grep { !looks_like_number($_) } keys %{$self->{+ITEMS}};
$self->{+ORDER} ||= [sort { $a <=> $b } keys %{$self->{+ITEMS}}];
croak "All indexes listed in the 'order' arrayref must be numeric"
if grep { !(looks_like_number($_) || (ref($_) && reftype($_) eq 'CODE')) } @{$self->{+ORDER}};
}
$self->{+FOR_EACH} ||= [];
$self->SUPER::init();
}
sub name { '<ARRAY>' }
sub meta_class { 'Test2::Compare::Meta' }
sub verify {
my $self = shift;
my %params = @_;
return 0 unless $params{exists};
my $got = $params{got};
return 0 unless defined $got;
return 0 unless ref($got);
return 0 unless reftype($got) eq 'ARRAY';
return 1;
}
sub add_prop {
my $self = shift;
$self->{+META} = $self->meta_class->new unless defined $self->{+META};
$self->{+META}->add_prop(@_);
}
sub top_index {
my $self = shift;
my @order = @{$self->{+ORDER}};
while(@order) {
my $idx = pop @order;
next if ref $idx;
return $idx;
}
return undef; # No indexes
}
sub add_item {
my $self = shift;
my $check = pop;
my ($idx) = @_;
my $top = $self->top_index;
croak "elements must be added in order!"
if $top && $idx && $idx <= $top;
$idx = defined($top) ? $top + 1 : 0
unless defined($idx);
push @{$self->{+ORDER}} => $idx;
$self->{+ITEMS}->{$idx} = $check;
}
sub add_filter {
my $self = shift;
my ($code) = @_;
croak "A single coderef is required"
unless @_ == 1 && $code && ref $code && reftype($code) eq 'CODE';
push @{$self->{+ORDER}} => $code;
}
sub add_for_each {
my $self = shift;
push @{$self->{+FOR_EACH}} => @_;
}
sub deltas {
my $self = shift;
my %params = @_;
my ($got, $convert, $seen) = @params{qw/got convert seen/};
my @deltas;
my $state = 0;
my @order = @{$self->{+ORDER}};
my $items = $self->{+ITEMS};
my $for_each = $self->{+FOR_EACH};
my $meta = $self->{+META};
push @deltas => $meta->deltas(%params) if defined $meta;
# Make a copy that we can munge as needed.
my @list = @$got;
while (@order) {
my $idx = shift @order;
my $overflow = 0;
my $val;
# We have a filter, not an index
if (ref($idx)) {
@list = $idx->(@list);
next;
}
confess "Internal Error: Stacks are out of sync (state > idx)"
if $state > $idx + 1;
while ($state <= $idx) {
$overflow = !@list;
$val = shift @list;
# check-all goes here so we hit each item, even unspecified ones.
for my $check (@$for_each) {
$check = $convert->($check);
push @deltas => $check->run(
id => [ARRAY => $state],
convert => $convert,
seen => $seen,
exists => !$overflow,
$overflow ? () : (got => $val),
);
}
$state++;
}
confess "Internal Error: Stacks are out of sync (state != idx + 1)"
unless $state == $idx + 1;
my $check = $convert->($items->{$idx});
push @deltas => $check->run(
id => [ARRAY => $idx],
convert => $convert,
seen => $seen,
exists => !$overflow,
$overflow ? () : (got => $val),
);
}
while (@list && (@$for_each || $self->{+ENDING})) {
my $item = shift @list;
for my $check (@$for_each) {
$check = $convert->($check);
push @deltas => $check->run(
id => [ARRAY => $state],
convert => $convert,
seen => $seen,
got => $item,
exists => 1,
);
}
# if items are left over, and ending is true, we have a problem!
if ($self->{+ENDING}) {
push @deltas => $self->delta_class->new(
dne => 'check',
verified => undef,
id => [ARRAY => $state],
got => $item,
check => undef,
$self->{+ENDING} eq 'implicit' ? (note => 'implicit end') : (),
);
}
$state++;
}
return @deltas;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Array - Internal representation of an array comparison.
=head1 DESCRIPTION
This module is an internal representation of an array for comparison purposes.
=head1 METHODS
=over 4
=item $ref = $arr->inref()
If the instance was constructed from an actual array, this will return the
reference to that array.
=item $bool = $arr->ending
=item $arr->set_ending($bool)
Set this to true if you would like to fail when the array being validated has
more items than the check. That is, if you check indexes 0-3 but the array has
values for indexes 0-4, it will fail and list that last item in the array as
unexpected. If set to false then it is assumed you do not care about extra
items.
=item $hashref = $arr->items()
Returns the hashref of C<< key => val >> pairs to be checked in the
array.
=item $arr->set_items($hashref)
Accepts a hashref to permit indexes to be skipped if desired.
B<Note:> that there is no validation when using C<set_items>, it is better to
use the C<add_item> interface.
=item $arrayref = $arr->order()
Returns an arrayref of all indexes that will be checked, in order.
=item $arr->set_order($arrayref)
Sets the order in which indexes will be checked.
B<Note:> that there is no validation when using C<set_order>, it is better to
use the C<add_item> interface.
=item $name = $arr->name()
Always returns the string C<< "<ARRAY>" >>.
=item $bool = $arr->verify(got => $got, exists => $bool)
Check if C<$got> is an array reference or not.
=item $idx = $arr->top_index()
Returns the topmost index which is checked. This will return undef if there
are no items, or C<0> if there is only 1 item.
=item $arr->add_item($item)
Push an item onto the list of values to be checked.
=item $arr->add_item($idx => $item)
Add an item to the list of values to be checked at the specified index.
=item $arr->add_filter(sub { ... })
Add a filter sub. The filter receives all remaining values of the array being
checked, and should return the values that should still be checked. The filter
will be run between the last item added and the next item added.
=item @deltas = $arr->deltas(got => $got, convert => \&convert, seen => \%seen)
Find the differences between the expected array values and those in the C<$got>
arrayref.
=back
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,244 @@
package Test2::Compare::Bag;
use strict;
use warnings;
use base 'Test2::Compare::Base';
our $VERSION = '0.000139';
use Test2::Util::HashBase qw/ending meta items for_each/;
use Carp qw/croak confess/;
use Scalar::Util qw/reftype looks_like_number/;
sub init {
my $self = shift;
$self->{+ITEMS} ||= [];
$self->{+FOR_EACH} ||= [];
$self->SUPER::init();
}
sub name { '<BAG>' }
sub meta_class { 'Test2::Compare::Meta' }
sub verify {
my $self = shift;
my %params = @_;
return 0 unless $params{exists};
my $got = $params{got} || return 0;
return 0 unless ref($got);
return 0 unless reftype($got) eq 'ARRAY';
return 1;
}
sub add_prop {
my $self = shift;
$self->{+META} = $self->meta_class->new unless defined $self->{+META};
$self->{+META}->add_prop(@_);
}
sub add_item {
my $self = shift;
my $check = pop;
my ($idx) = @_;
push @{$self->{+ITEMS}}, $check;
}
sub add_for_each {
my $self = shift;
push @{$self->{+FOR_EACH}} => @_;
}
sub deltas {
my $self = shift;
my %params = @_;
my ($got, $convert, $seen) = @params{qw/got convert seen/};
my @deltas;
my $state = 0;
my @items = @{$self->{+ITEMS}};
my @for_each = @{$self->{+FOR_EACH}};
# Make a copy that we can munge as needed.
my @list = @$got;
my %unmatched = map { $_ => $list[$_] } 0..$#list;
my $meta = $self->{+META};
push @deltas => $meta->deltas(%params) if defined $meta;
while (@items) {
my $item = shift @items;
my $check = $convert->($item);
my $match = 0;
for my $idx (0..$#list) {
next unless exists $unmatched{$idx};
my $val = $list[$idx];
my $deltas = $check->run(
id => [ARRAY => $idx],
convert => $convert,
seen => $seen,
exists => 1,
got => $val,
);
unless ($deltas) {
$match++;
delete $unmatched{$idx};
last;
}
}
unless ($match) {
push @deltas => $self->delta_class->new(
dne => 'got',
verified => undef,
id => [ARRAY => '*'],
got => undef,
check => $check,
);
}
}
if (@for_each) {
my @checks = map { $convert->($_) } @for_each;
for my $idx (0..$#list) {
# All items are matched if we have conditions for all items
delete $unmatched{$idx};
my $val = $list[$idx];
for my $check (@checks) {
push @deltas => $check->run(
id => [ARRAY => $idx],
convert => $convert,
seen => $seen,
exists => 1,
got => $val,
);
}
}
}
# if elements are left over, and ending is true, we have a problem!
if($self->{+ENDING} && keys %unmatched) {
for my $idx (sort keys %unmatched) {
my $elem = $list[$idx];
push @deltas => $self->delta_class->new(
dne => 'check',
verified => undef,
id => [ARRAY => $idx],
got => $elem,
check => undef,
$self->{+ENDING} eq 'implicit' ? (note => 'implicit end') : (),
);
}
}
return @deltas;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Bag - Internal representation of a bag comparison.
=head1 DESCRIPTION
This module is an internal representation of a bag for comparison purposes.
=head1 METHODS
=over 4
=item $bool = $arr->ending
=item $arr->set_ending($bool)
Set this to true if you would like to fail when the array being validated has
more items than the check. That is, if you check for 4 items but the array has
5 values, it will fail and list that unmatched item in the array as
unexpected. If set to false then it is assumed you do not care about extra
items.
=item $arrayref = $arr->items()
Returns the arrayref of values to be checked in the array.
=item $arr->set_items($arrayref)
Accepts an arrayref.
B<Note:> that there is no validation when using C<set_items>, it is better to
use the C<add_item> interface.
=item $name = $arr->name()
Always returns the string C<< "<BAG>" >>.
=item $bool = $arr->verify(got => $got, exists => $bool)
Check if C<$got> is an array reference or not.
=item $arr->add_item($item)
Push an item onto the list of values to be checked.
=item @deltas = $arr->deltas(got => $got, convert => \&convert, seen => \%seen)
Find the differences between the expected bag values and those in the C<$got>
arrayref.
=back
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=item Gianni Ceccarelli E<lt>dakkar@thenautilus.netE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=item Gianni Ceccarelli E<lt>dakkar@thenautilus.netE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
Copyright 2018 Gianni Ceccarelli E<lt>dakkar@thenautilus.netE<gt>
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,252 @@
package Test2::Compare::Base;
use strict;
use warnings;
our $VERSION = '0.000139';
use Carp qw/confess croak/;
use Scalar::Util qw/blessed/;
use Sub::Info qw/sub_info/;
use Test2::Compare::Delta();
sub MAX_CYCLES() { 75 }
use Test2::Util::HashBase qw{builder _file _lines _info called};
use Test2::Util::Ref qw/render_ref/;
{
no warnings 'once';
*set_lines = \&set__lines;
*set_file = \&set__file;
}
sub clone {
my $self = shift;
my $class = blessed($self);
# Shallow copy is good enough for all the current compare types.
return bless({%$self}, $class);
}
sub init {
my $self = shift;
$self->{+_LINES} = delete $self->{lines} if exists $self->{lines};
$self->{+_FILE} = delete $self->{file} if exists $self->{file};
}
sub file {
my $self = shift;
return $self->{+_FILE} if $self->{+_FILE};
if ($self->{+BUILDER}) {
$self->{+_INFO} ||= sub_info($self->{+BUILDER});
return $self->{+_INFO}->{file};
}
elsif ($self->{+CALLED}) {
return $self->{+CALLED}->[1];
}
return undef;
}
sub lines {
my $self = shift;
return $self->{+_LINES} if $self->{+_LINES};
if ($self->{+BUILDER}) {
$self->{+_INFO} ||= sub_info($self->{+BUILDER});
return $self->{+_INFO}->{lines} if @{$self->{+_INFO}->{lines}};
}
if ($self->{+CALLED}) {
return [$self->{+CALLED}->[2]];
}
return [];
}
sub delta_class { 'Test2::Compare::Delta' }
sub deltas { () }
sub got_lines { () }
sub stringify_got { 0 }
sub operator { '' }
sub verify { confess "unimplemented" }
sub name { confess "unimplemented" }
sub render {
my $self = shift;
return $self->name;
}
sub run {
my $self = shift;
my %params = @_;
my $id = $params{id};
my $convert = $params{convert} or confess "no convert sub provided";
my $seen = $params{seen} ||= {};
$params{exists} = exists $params{got} ? 1 : 0
unless exists $params{exists};
my $exists = $params{exists};
my $got = $exists ? $params{got} : undef;
my $gotname = render_ref($got);
# Prevent infinite cycles
if (defined($got) && ref $got) {
die "Cycle detected in comparison, aborting"
if $seen->{$gotname} && $seen->{$gotname} >= MAX_CYCLES;
$seen->{$gotname}++;
}
my $ok = $self->verify(%params);
my @deltas = $ok ? $self->deltas(%params) : ();
$seen->{$gotname}-- if defined $got && ref $got;
return if $ok && !@deltas;
return $self->delta_class->new(
verified => $ok,
id => $id,
got => $got,
check => $self,
children => \@deltas,
$exists ? () : (dne => 'got'),
);
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Base - Base class for comparison classes.
=head1 DESCRIPTION
All comparison classes for Test2::Compare should inherit from this base class.
=head1 SYNOPSIS
package Test2::Compare::MyCheck;
use strict;
use warnings;
use base 'Test2::Compare::Base';
use Test2::Util::HashBase qw/stuff/;
sub name { 'STUFF' }
sub operator {
my $self = shift;
my ($got) = @_;
return 'eq';
}
sub verify {
my $self = shift;
my $params = @_;
# Always check if $got exists! This method must return false if no
# value at all was received.
return 0 unless $params{exists};
my $got = $params{got};
# Returns true if both values match. This includes undef, 0, and other
# false-y values!
return $got eq $self->stuff;
}
=head1 METHODS
Some of these must be overridden, others can be.
=over 4
=item $dclass = $check->delta_class
Returns the delta subclass that should be used. By default
L<Test2::Compare::Delta> is used.
=item @deltas = $check->deltas(id => $id, exists => $bool, got => $got, convert => \&convert, seen => \%seen)
Should return child deltas.
=item @lines = $check->got_lines($got)
This is your chance to provide line numbers for errors in the C<$got>
structure.
=item $op = $check->operator()
=item $op = $check->operator($got)
Returns the operator that was used to compare the check with the received data
in C<$got>. If there was no value for got then there will be no arguments,
undef will only be an argument if undef was seen in C<$got>. This is how you
can tell the difference between a missing value and an undefined one.
=item $bool = $check->verify(id => $id, exists => $bool, got => $got, convert => \&convert, seen => \%seen)
Return true if there is a shallow match, that is both items are arrayrefs, both
items are the same string or same number, etc. This should not recurse, as deep
checks are done in C<< $check->deltas() >>.
=item $name = $check->name
Get the name of the check.
=item $display = $check->render
What should be displayed in a table for this check, usually the name or value.
=item $delta = $check->run(id => $id, exists => $bool, got => $got, convert => \&convert, seen => \%seen)
This is where the checking is done, first a shallow check using
C<< $check->verify >>, then checking C<< $check->deltas() >>. C<\%seen> is used
to prevent cycles.
=back
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,111 @@
package Test2::Compare::Bool;
use strict;
use warnings;
use Carp qw/confess/;
use base 'Test2::Compare::Base';
our $VERSION = '0.000139';
use Test2::Util::HashBase qw/input/;
# Overloads '!' for us.
use Test2::Compare::Negatable;
sub name {
my $self = shift;
my $in = $self->{+INPUT};
return _render_bool($in);
}
sub operator {
my $self = shift;
return '!=' if $self->{+NEGATE};
return '==';
}
sub verify {
my $self = shift;
my %params = @_;
my ($got, $exists) = @params{qw/got exists/};
return 0 unless $exists;
my $want = $self->{+INPUT};
my $match = ($want xor $got) ? 0 : 1;
$match = $match ? 0 : 1 if $self->{+NEGATE};
return $match;
}
sub run {
my $self = shift;
my $delta = $self->SUPER::run(@_) or return;
my $dne = $delta->dne || "";
unless ($dne eq 'got') {
my $got = $delta->got;
$delta->set_got(_render_bool($got));
}
return $delta;
}
sub _render_bool {
my $bool = shift;
my $name = $bool ? 'TRUE' : 'FALSE';
my $val = defined $bool ? $bool : 'undef';
$val = "''" unless length($val);
return "<$name ($val)>";
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Bool - Compare two values as booleans
=head1 DESCRIPTION
Check if two values have the same boolean result (both true, or both false).
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,173 @@
package Test2::Compare::Custom;
use strict;
use warnings;
use base 'Test2::Compare::Base';
our $VERSION = '0.000139';
use Test2::Util::HashBase qw/code name operator/;
use Carp qw/croak/;
sub init {
my $self = shift;
croak "'code' is required" unless $self->{+CODE};
$self->{+OPERATOR} ||= 'CODE(...)';
$self->{+NAME} ||= '<Custom Code>';
$self->SUPER::init();
}
sub verify {
my $self = shift;
my %params = @_;
my ($got, $exists) = @params{qw/got exists/};
my $code = $self->{+CODE};
local $_ = $got;
my $ok = $code->(
got => $got,
exists => $exists,
operator => $self->{+OPERATOR},
name => $self->{+NAME},
);
return $ok;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Custom - Custom field check for comparisons.
=head1 DESCRIPTION
Sometimes you want to do something complicated or unusual when validating a
field nested inside a deep data structure. You could pull it out of the
structure and test it separately, or you can use this to embed the check. This
provides a way for you to write custom checks for fields in deep comparisons.
=head1 SYNOPSIS
my $cus = Test2::Compare::Custom->new(
name => 'IsRef',
operator => 'ref(...)',
code => sub {
my %args = @_;
return $args{got} ? 1 : 0;
},
);
# Pass
is(
{ a => 1, ref => {}, b => 2 },
{ a => 1, ref => $cus, b => 2 },
"This will pass"
);
# Fail
is(
{a => 1, ref => 'notref', b => 2},
{a => 1, ref => $cus, b => 2},
"This will fail"
);
=head1 ARGUMENTS
Your custom sub will be passed 4 arguments in a hash:
code => sub {
my %args = @_;
# provides got, exists, operator, name
return ref($args{got}) ? 1 : 0;
},
C<$_> is also localized to C<got> to make it easier for those who need to use
regexes.
=over 4
=item got
=item $_
The value to be checked.
=item exists
This will be a boolean. This will be true if C<got> exists at all. If
C<exists> is false then it means C<got> is not simply undef, but doesn't
exist at all (think checking the value of a hash key that does not exist).
=item operator
The operator specified at construction.
=item name
The name provided at construction.
=back
=head1 METHODS
=over 4
=item $code = $cus->code
Returns the coderef provided at construction.
=item $name = $cus->name
Returns the name provided at construction.
=item $op = $cus->operator
Returns the operator provided at construction.
=item $bool = $cus->verify(got => $got, exists => $bool)
=back
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,119 @@
package Test2::Compare::DeepRef;
use strict;
use warnings;
use base 'Test2::Compare::Base';
our $VERSION = '0.000139';
use Test2::Util::HashBase qw/input/;
use Test2::Util::Ref qw/render_ref rtype/;
use Scalar::Util qw/refaddr/;
use Carp qw/croak/;
sub init {
my $self = shift;
croak "'input' is a required attribute"
unless $self->{+INPUT};
croak "'input' must be a reference, got '" . $self->{+INPUT} . "'"
unless ref $self->{+INPUT};
$self->SUPER::init();
}
sub name { '<REF>' }
sub verify {
my $self = shift;
my %params = @_;
my ($got, $exists) = @params{qw/got exists/};
return 0 unless $exists;
my $in = $self->{+INPUT};
return 0 unless ref $in;
return 0 unless ref $got;
my $in_type = rtype($in);
my $got_type = rtype($got);
return 0 unless $in_type eq $got_type;
return 1;
}
sub deltas {
my $self = shift;
my %params = @_;
my ($got, $convert, $seen) = @params{qw/got convert seen/};
my $in = $self->{+INPUT};
my $in_type = rtype($in);
my $got_type = rtype($got);
my $check = $convert->($$in);
return $check->run(
id => ['DEREF' => '$*'],
convert => $convert,
seen => $seen,
got => $$got,
exists => 1,
);
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::DeepRef - Ref comparison
=head1 DESCRIPTION
Used to compare two refs in a deep comparison.
=head1 SYNOPSIS
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,558 @@
package Test2::Compare::Delta;
use strict;
use warnings;
our $VERSION = '0.000139';
use Test2::Util::HashBase qw{verified id got chk children dne exception note};
use Test2::EventFacet::Info::Table;
use Test2::Util::Table();
use Test2::API qw/context/;
use Test2::Util::Ref qw/render_ref rtype/;
use Carp qw/croak/;
# 'CHECK' constant would not work, but I like exposing 'check()' to people
# using this class.
BEGIN {
no warnings 'once';
*check = \&chk;
*set_check = \&set_chk;
}
my @COLUMN_ORDER = qw/PATH GLNs GOT OP CHECK CLNs/;
my %COLUMNS = (
GOT => {name => 'GOT', value => sub { $_[0]->render_got }, no_collapse => 1},
CHECK => {name => 'CHECK', value => sub { $_[0]->render_check }, no_collapse => 1},
OP => {name => 'OP', value => sub { $_[0]->table_op } },
PATH => {name => 'PATH', value => sub { $_[1] } },
'GLNs' => {name => 'GLNs', alias => 'LNs', value => sub { $_[0]->table_got_lines } },
'CLNs' => {name => 'CLNs', alias => 'LNs', value => sub { $_[0]->table_check_lines }},
);
{
my $i = 0;
$COLUMNS{$_}->{id} = $i++ for @COLUMN_ORDER;
}
sub remove_column {
my $class = shift;
my $header = shift;
@COLUMN_ORDER = grep { $_ ne $header } @COLUMN_ORDER;
delete $COLUMNS{$header} ? 1 : 0;
}
sub add_column {
my $class = shift;
my $name = shift;
croak "Column name is required"
unless $name;
croak "Column '$name' is already defined"
if $COLUMNS{$name};
my %params;
if (@_ == 1) {
%params = (value => @_, name => $name);
}
else {
%params = (@_, name => $name);
}
my $value = $params{value};
croak "You must specify a 'value' callback"
unless $value;
croak "'value' callback must be a CODE reference"
unless rtype($value) eq 'CODE';
if ($params{prefix}) {
unshift @COLUMN_ORDER => $name;
}
else {
push @COLUMN_ORDER => $name;
}
$COLUMNS{$name} = \%params;
}
sub set_column_alias {
my ($class, $name, $alias) = @_;
croak "Tried to alias a non-existent column"
unless exists $COLUMNS{$name};
croak "Missing alias" unless defined $alias;
$COLUMNS{$name}->{alias} = $alias;
}
sub init {
my $self = shift;
croak "Cannot specify both 'check' and 'chk' as arguments"
if exists($self->{check}) && exists($self->{+CHK});
# Allow 'check' as an argument
$self->{+CHK} ||= delete $self->{check}
if exists $self->{check};
}
sub render_got {
my $self = shift;
my $exp = $self->{+EXCEPTION};
if ($exp) {
chomp($exp = "$exp");
$exp =~ s/\n.*$//g;
return "<EXCEPTION: $exp>";
}
my $dne = $self->{+DNE};
return '<DOES NOT EXIST>' if $dne && $dne eq 'got';
my $got = $self->{+GOT};
return '<UNDEF>' unless defined $got;
my $check = $self->{+CHK};
my $stringify = defined( $check ) && $check->stringify_got;
return render_ref($got) if ref $got && !$stringify;
return "$got";
}
sub render_check {
my $self = shift;
my $dne = $self->{+DNE};
return '<DOES NOT EXIST>' if $dne && $dne eq 'check';
my $check = $self->{+CHK};
return '<UNDEF>' unless defined $check;
return $check->render;
}
sub _full_id {
my ($type, $id) = @_;
return "<$id>" if !$type || $type eq 'META';
return $id if $type eq 'SCALAR';
return "{$id}" if $type eq 'HASH';
return "{$id} <KEY>" if $type eq 'HASHKEY';
return "[$id]" if $type eq 'ARRAY';
return "$id()" if $type eq 'METHOD';
return "$id" if $type eq 'DEREF';
return "<$id>";
}
sub _arrow_id {
my ($path, $type) = @_;
return '' unless $path;
return ' ' if !$type || $type eq 'META'; # Meta gets a space, not an arrow
return '->' if $type eq 'METHOD'; # Method always needs an arrow
return '->' if $type eq 'SCALAR'; # Scalar always needs an arrow
return '->' if $type eq 'DEREF'; # deref always needs arrow
return '->' if $path =~ m/(>|\(\))$/; # Need an arrow after meta, or after a method
return '->' if $path eq '$VAR'; # Need an arrow after the initial ref
# Hash and array need an arrow unless they follow another hash/array
return '->' if $type =~ m/^(HASH|ARRAY)$/ && $path !~ m/(\]|\})$/;
# No arrow needed
return '';
}
sub _join_id {
my ($path, $parts) = @_;
my ($type, $key) = @$parts;
my $id = _full_id($type, $key);
my $join = _arrow_id($path, $type);
return "${path}${join}${id}";
}
sub should_show {
my $self = shift;
return 1 unless $self->verified;
defined( my $check = $self->check ) || return 0;
return 0 unless $check->lines;
my $file = $check->file || return 0;
my $ctx = context();
my $cfile = $ctx->trace->file;
$ctx->release;
return 0 unless $file eq $cfile;
return 1;
}
sub filter_visible {
my $self = shift;
my @deltas;
my @queue = (['', $self]);
while (my $set = shift @queue) {
my ($path, $delta) = @$set;
push @deltas => [$path, $delta] if $delta->should_show;
my $children = $delta->children || next;
next unless @$children;
my @new;
for my $child (@$children) {
my $cpath = _join_id($path, $child->id);
push @new => [$cpath, $child];
}
unshift @queue => @new;
}
return \@deltas;
}
sub table_header { [map {$COLUMNS{$_}->{alias} || $_} @COLUMN_ORDER] }
sub table_op {
my $self = shift;
defined( my $check = $self->{+CHK} ) || return '!exists';
return $check->operator($self->{+GOT})
unless $self->{+DNE} && $self->{+DNE} eq 'got';
return $check->operator();
}
sub table_check_lines {
my $self = shift;
defined( my $check = $self->{+CHK} ) || return '';
my $lines = $check->lines || return '';
return '' unless @$lines;
return join ', ' => @$lines;
}
sub table_got_lines {
my $self = shift;
defined( my $check = $self->{+CHK} ) || return '';
return '' if $self->{+DNE} && $self->{+DNE} eq 'got';
my @lines = $check->got_lines($self->{+GOT});
return '' unless @lines;
return join ', ' => @lines;
}
sub table_rows {
my $self = shift;
my $deltas = $self->filter_visible;
my @rows;
for my $set (@$deltas) {
my ($id, $d) = @$set;
my @row;
for my $col (@COLUMN_ORDER) {
my $spec = $COLUMNS{$col};
my $val = $spec->{value}->($d, $id);
$val = '' unless defined $val;
push @row => $val;
}
push @rows => \@row;
}
return \@rows;
}
sub table {
my $self = shift;
my @diag;
my $header = $self->table_header;
my $rows = $self->table_rows;
my $render_rows = [@$rows];
my $max = exists $ENV{TS_MAX_DELTA} ? $ENV{TS_MAX_DELTA} : 25;
if ($max && @$render_rows > $max) {
@$render_rows = map { [@$_] } @{$render_rows}[0 .. ($max - 1)];
@diag = (
"************************************************************",
sprintf("* Stopped after %-42.42s *", "$max differences."),
"* Set the TS_MAX_DELTA environment var to raise the limit. *",
"* Set it to 0 for no limit. *",
"************************************************************",
);
}
my @dne;
for my $row (@$render_rows) {
my $got = $row->[$COLUMNS{GOT}->{id}] || '';
my $chk = $row->[$COLUMNS{CHECK}->{id}] || '';
if ($got eq '<DOES NOT EXIST>') {
push @dne => "$row->[$COLUMNS{PATH}->{id}]: DOES NOT EXIST";
}
elsif ($chk eq '<DOES NOT EXIST>') {
push @dne => "$row->[$COLUMNS{PATH}->{id}]: SHOULD NOT EXIST";
}
}
if (@dne) {
unshift @dne => '==== Summary of missing/extra items ====';
push @dne => '== end summary of missing/extra items ==';
}
my $table_args = {
header => $header,
collapse => 1,
sanitize => 1,
mark_tail => 1,
no_collapse => [grep { $COLUMNS{$COLUMN_ORDER[$_]}->{no_collapse} } 0 .. $#COLUMN_ORDER],
};
my $render = join "\n" => (
Test2::Util::Table::table(%$table_args, rows => $render_rows),
@dne,
@diag,
);
my $table = Test2::EventFacet::Info::Table->new(
%$table_args,
rows => $rows,
as_string => $render,
);
return $table;
}
sub diag { shift->table }
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Delta - Representation of differences between nested data
structures.
=head1 DESCRIPTION
This is used by L<Test2::Compare>. When data structures are compared a
delta will be returned. Deltas are a tree data structure that represent all the
differences between two other data structures.
=head1 METHODS
=head2 CLASS METHODS
=over 4
=item $class->add_column($NAME => sub { ... })
=item $class->add_column($NAME, %PARAMS)
This can be used to add columns to the table that it produced when a comparison
fails. The first argument should always be the column name, which must be
unique.
The first form simply takes a coderef that produces the value that should be
displayed in the column for any given delta. The arguments passed into the sub
are the delta, and the row ID.
Test2::Compare::Delta->add_column(
Foo => sub {
my ($delta, $id) = @_;
return $delta->... ? 'foo' : 'bar'
},
);
The second form allows you some extra options. The C<'value'> key is required,
and must be a coderef. All other keys are optional.
Test2::Compare::Delta->add_column(
'Foo', # column name
value => sub { ... }, # how to get the cell value
alias => 'FOO', # Display name (used in table header)
no_collapse => $bool, # Show column even if it has no values?
);
=item $bool = $class->remove_column($NAME)
This will remove the specified column. This will return true if the column
existed and was removed. This will return false if the column did not exist. No
exceptions are thrown. If a missing column is a problem then you need to check
the return yourself.
=item $class->set_column_alias($NAME, $ALIAS)
This can be used to change the table header, overriding the default column
names with new ones.
=back
=head2 ATTRIBUTES
=over 4
=item $bool = $delta->verified
=item $delta->set_verified($bool)
This will be true if the delta itself matched, if the delta matched then the
problem is in the delta's children, not the delta itself.
=item $aref = $delta->id
=item $delta->set_id([$type, $name])
ID for the delta, used to produce the path into the data structure. An
example is C<< ['HASH' => 'foo'] >> which means the delta is in the path
C<< ...->{'foo'} >>. Valid types are C<HASH>, C<ARRAY>, C<SCALAR>, C<META>, and
C<METHOD>.
=item $val = $delta->got
=item $delta->set_got($val)
Deltas are produced by comparing a received data structure 'got' against a
check data structure 'check'. The 'got' attribute contains the value that was
received for comparison.
=item $check = $delta->chk
=item $check = $delta->check
=item $delta->set_chk($check)
=item $delta->set_check($check)
Deltas are produced by comparing a received data structure 'got' against a
check data structure 'check'. The 'check' attribute contains the value that was
expected in the comparison.
C<check> and C<chk> are aliases for the same attribute.
=item $aref = $delta->children
=item $delta->set_children([$delta1, $delta2, ...])
A Delta may have child deltas. If it does then this is an arrayref with those
children.
=item $dne = $delta->dne
=item $delta->set_dne($dne)
Sometimes a comparison results in one side or the other not existing at all, in
which case this is set to the name of the attribute that does not exist. This
can be set to 'got' or 'check'.
=item $e = $delta->exception
=item $delta->set_exception($e)
This will be set to the exception in cases where the comparison failed due to
an exception being thrown.
=back
=head2 OTHER
=over 4
=item $string = $delta->render_got
Renders the string that should be used in a table to represent the received
value in a comparison.
=item $string = $delta->render_check
Renders the string that should be used in a table to represent the expected
value in a comparison.
=item $bool = $delta->should_show
This will return true if the delta should be shown in the table. This is
normally true for any unverified delta. This will also be true for deltas that
contain extra useful debug information.
=item $aref = $delta->filter_visible
This will produce an arrayref of C<< [ $path => $delta ] >> for all deltas that
should be displayed in the table.
=item $aref = $delta->table_header
This returns an array ref of the headers for the table.
=item $string = $delta->table_op
This returns the operator that should be shown in the table.
=item $string = $delta->table_check_lines
This returns the defined lines (extra debug info) that should be displayed.
=item $string = $delta->table_got_lines
This returns the generated lines (extra debug info) that should be displayed.
=item $aref = $delta->table_rows
This returns an arrayref of table rows, each row is itself an arrayref.
=item @table_lines = $delta->table
Returns all the lines of the table that should be displayed.
=back
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,81 @@
package Test2::Compare::Event;
use strict;
use warnings;
use Scalar::Util qw/blessed/;
use Test2::Compare::EventMeta();
use base 'Test2::Compare::Object';
our $VERSION = '0.000139';
use Test2::Util::HashBase qw/etype/;
sub name {
my $self = shift;
my $etype = $self->etype;
return "<EVENT: $etype>";
}
sub meta_class { 'Test2::Compare::EventMeta' }
sub object_base { 'Test2::Event' }
sub got_lines {
my $self = shift;
my ($event) = @_;
return unless $event;
return unless blessed($event);
return unless $event->isa('Test2::Event');
return unless $event->trace;
return ($event->trace->line);
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Event - Event specific Object subclass.
=head1 DESCRIPTION
This module is used to represent an expected event in a deep comparison.
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,100 @@
package Test2::Compare::EventMeta;
use strict;
use warnings;
use base 'Test2::Compare::Meta';
our $VERSION = '0.000139';
use Test2::Util::HashBase;
sub get_prop_file { $_[1]->trace->file }
sub get_prop_line { $_[1]->trace->line }
sub get_prop_package { $_[1]->trace->package }
sub get_prop_subname { $_[1]->trace->subname }
sub get_prop_debug { $_[1]->trace->debug }
sub get_prop_tid { $_[1]->trace->tid }
sub get_prop_pid { $_[1]->trace->pid }
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::EventMeta - Meta class for events in deep comparisons
=head1 DESCRIPTION
This is used in deep comparisons of event objects. You should probably never
use this directly.
=head1 DEFINED CHECKS
=over 4
=item file
File that generated the event.
=item line
Line where the event was generated.
=item package
Package that generated the event.
=item subname
Name of the tool that generated the event.
=item debug
The debug information that will be printed in event of a failure.
=item tid
Thread ID of the thread that generated the event.
=item pid
Process ID of the process that generated the event.
=back
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,177 @@
package Test2::Compare::Float;
use strict;
use warnings;
use Carp qw/confess/;
use base 'Test2::Compare::Base';
our $VERSION = '0.000139';
our $DEFAULT_TOLERANCE = 1e-08;
use Test2::Util::HashBase qw/input tolerance precision/;
# Overloads '!' for us.
use Test2::Compare::Negatable;
sub init {
my $self = shift;
my $input = $self->{+INPUT};
if ( exists $self->{+TOLERANCE} and exists $self->{+PRECISION} ) {
confess "can't set both tolerance and precision";
} elsif (!exists $self->{+PRECISION} and !exists $self->{+TOLERANCE}) {
$self->{+TOLERANCE} = $DEFAULT_TOLERANCE
}
confess "input must be defined for 'Float' check"
unless defined $input;
# Check for ''
confess "input must be a number for 'Float' check"
unless length($input) && $input =~ m/\S/;
confess "precision must be an integer for 'Float' check"
if exists $self->{+PRECISION} && $self->{+PRECISION} !~ m/^\d+$/;
$self->SUPER::init(@_);
}
sub name {
my $self = shift;
my $in = $self->{+INPUT};
my $precision = $self->{+PRECISION};
if ( defined $precision) {
return sprintf "%.*f", $precision, $in;
}
my $tolerance = $self->{+TOLERANCE};
return "$in +/- $tolerance";
}
sub operator {
my $self = shift;
return '' unless @_;
my ($got) = @_;
return '' unless defined($got);
return '' unless length($got) && $got =~ m/\S/;
if ( $self->{+PRECISION} )
{
return 'ne' if $self->{+NEGATE};
return 'eq';
}
return '!=' if $self->{+NEGATE};
return '==';
}
sub verify {
my $self = shift;
my %params = @_;
my ($got, $exists) = @params{qw/got exists/};
return 0 unless $exists;
return 0 unless defined $got;
return 0 if ref $got;
return 0 unless length($got) && $got =~ m/\S/;
my $input = $self->{+INPUT};
my $negate = $self->{+NEGATE};
my $tolerance = $self->{+TOLERANCE};
my $precision = $self->{+PRECISION};
my @warnings;
my $out;
{
local $SIG{__WARN__} = sub { push @warnings => @_ };
my $equal = ($input == $got);
if (!$equal) {
if (defined $tolerance) {
$equal = 1 if
$got > $input - $tolerance &&
$got < $input + $tolerance;
} else {
$equal =
sprintf("%.*f", $precision, $got) eq
sprintf("%.*f", $precision, $input);
}
}
$out = $negate ? !$equal : $equal;
}
for my $warn (@warnings) {
if ($warn =~ m/numeric/) {
$out = 0;
next; # This warning won't help anyone.
}
warn $warn;
}
return $out;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Float - Compare two values as numbers with tolerance.
=head1 DESCRIPTION
This is used to compare two numbers. You can also check that two numbers are not
the same.
This is similar to Test2::Compare::Number, with extra checks to work around floating
point representation issues.
The optional 'tolerance' parameter controls how close the two numbers must be to
be considered equal. Tolerance defaults to 1e-08.
B<Note>: This will fail if the received value is undefined. It must be a number.
B<Note>: This will fail if the comparison generates a non-numeric value warning
(which will not be shown). This is because it must get a number. The warning is
not shown as it will report to a useless line and filename. However, the test
diagnostics show both values.
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Andrew Grangaard E<lt>spazm@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,238 @@
package Test2::Compare::Hash;
use strict;
use warnings;
use base 'Test2::Compare::Base';
our $VERSION = '0.000139';
use Test2::Util::HashBase qw/inref meta ending items order for_each_key for_each_val/;
use Carp qw/croak confess/;
use Scalar::Util qw/reftype/;
sub init {
my $self = shift;
if( defined( my $ref = $self->{+INREF} ) ) {
croak "Cannot specify both 'inref' and 'items'" if $self->{+ITEMS};
croak "Cannot specify both 'inref' and 'order'" if $self->{+ORDER};
$self->{+ITEMS} = {%$ref};
$self->{+ORDER} = [sort keys %$ref];
}
else {
# Clone the ref to be safe
$self->{+ITEMS} = $self->{+ITEMS} ? {%{$self->{+ITEMS}}} : {};
if ($self->{+ORDER}) {
my @all = keys %{$self->{+ITEMS}};
my %have = map { $_ => 1 } @{$self->{+ORDER}};
my @missing = grep { !$have{$_} } @all;
croak "Keys are missing from the 'order' array: " . join(', ', sort @missing)
if @missing;
}
else {
$self->{+ORDER} = [sort keys %{$self->{+ITEMS}}];
}
}
$self->{+FOR_EACH_KEY} ||= [];
$self->{+FOR_EACH_VAL} ||= [];
$self->SUPER::init();
}
sub name { '<HASH>' }
sub meta_class { 'Test2::Compare::Meta' }
sub verify {
my $self = shift;
my %params = @_;
my ($got, $exists) = @params{qw/got exists/};
return 0 unless $exists;
return 0 unless defined $got;
return 0 unless ref($got);
return 0 unless reftype($got) eq 'HASH';
return 1;
}
sub add_prop {
my $self = shift;
$self->{+META} = $self->meta_class->new unless defined $self->{+META};
$self->{+META}->add_prop(@_);
}
sub add_field {
my $self = shift;
my ($name, $check) = @_;
croak "field name is required"
unless defined $name;
croak "field '$name' has already been specified"
if exists $self->{+ITEMS}->{$name};
push @{$self->{+ORDER}} => $name;
$self->{+ITEMS}->{$name} = $check;
}
sub add_for_each_key {
my $self = shift;
push @{$self->{+FOR_EACH_KEY}} => @_;
}
sub add_for_each_val {
my $self = shift;
push @{$self->{+FOR_EACH_VAL}} => @_;
}
sub deltas {
my $self = shift;
my %params = @_;
my ($got, $convert, $seen) = @params{qw/got convert seen/};
my @deltas;
my $items = $self->{+ITEMS};
my $each_key = $self->{+FOR_EACH_KEY};
my $each_val = $self->{+FOR_EACH_VAL};
# Make a copy that we can munge as needed.
my %fields = %$got;
my $meta = $self->{+META};
push @deltas => $meta->deltas(%params) if defined $meta;
for my $key (@{$self->{+ORDER}}) {
my $check = $convert->($items->{$key});
my $exists = exists $fields{$key};
my $val = delete $fields{$key};
if ($exists) {
for my $kcheck (@$each_key) {
$kcheck = $convert->($kcheck);
push @deltas => $kcheck->run(
id => [HASHKEY => $key],
convert => $convert,
seen => $seen,
exists => $exists,
got => $key,
);
}
for my $vcheck (@$each_val) {
$vcheck = $convert->($vcheck);
push @deltas => $vcheck->run(
id => [HASH => $key],
convert => $convert,
seen => $seen,
exists => $exists,
got => $val,
);
}
}
push @deltas => $check->run(
id => [HASH => $key],
convert => $convert,
seen => $seen,
exists => $exists,
$exists ? (got => $val) : (),
);
}
if (keys %fields) {
for my $key (sort keys %fields) {
my $val = $fields{$key};
for my $kcheck (@$each_key) {
$kcheck = $convert->($kcheck);
push @deltas => $kcheck->run(
id => [HASHKEY => $key],
convert => $convert,
seen => $seen,
got => $key,
exists => 1,
);
}
for my $vcheck (@$each_val) {
$vcheck = $convert->($vcheck);
push @deltas => $vcheck->run(
id => [HASH => $key],
convert => $convert,
seen => $seen,
got => $val,
exists => 1,
);
}
# if items are left over, and ending is true, we have a problem!
if ($self->{+ENDING}) {
push @deltas => $self->delta_class->new(
dne => 'check',
verified => undef,
id => [HASH => $key],
got => $val,
check => undef,
$self->{+ENDING} eq 'implicit' ? (note => 'implicit end') : (),
);
}
}
}
return @deltas;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Hash - Representation of a hash in a deep comparison.
=head1 DESCRIPTION
In deep comparisons this class is used to represent a hash.
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,100 @@
package Test2::Compare::Isa;
use strict;
use warnings;
use Carp qw/confess/;
use Scalar::Util qw/blessed/;
use base 'Test2::Compare::Base';
our $VERSION = '0.000139';
use Test2::Util::HashBase qw/input/;
# Overloads '!' for us.
use Test2::Compare::Negatable;
sub init {
my $self = shift;
confess "input must be defined for 'Isa' check"
unless defined $self->{+INPUT};
$self->SUPER::init(@_);
}
sub name {
my $self = shift;
my $in = $self->{+INPUT};
return "$in";
}
sub operator {
my $self = shift;
return '!isa' if $self->{+NEGATE};
return 'isa';
}
sub verify {
my $self = shift;
my %params = @_;
my ($got, $exists) = @params{qw/got exists/};
return 0 unless $exists;
my $input = $self->{+INPUT};
my $negate = $self->{+NEGATE};
my $isa = blessed($got) && $got->isa($input);
return !$isa if $negate;
return $isa;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Isa - Check if the value is an instance of the class.
=head1 DESCRIPTION
This is used to check if the got value is an instance of the expected class.
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=item TOYAMA Nao E<lt>nanto@moon.email.ne.jpE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,183 @@
package Test2::Compare::Meta;
use strict;
use warnings;
use Test2::Compare::Delta();
use Test2::Compare::Isa();
use base 'Test2::Compare::Base';
our $VERSION = '0.000139';
use Test2::Util::HashBase qw/items/;
use Carp qw/croak confess/;
use Scalar::Util qw/reftype blessed/;
sub init {
my $self = shift;
$self->{+ITEMS} ||= [];
$self->SUPER::init();
}
sub name { '<META CHECKS>' }
sub verify {
my $self = shift;
my %params = @_;
return $params{exists} ? 1 : 0;
}
sub add_prop {
my $self = shift;
my ($name, $check) = @_;
croak "prop name is required"
unless defined $name;
croak "check is required"
unless defined $check;
my $meth = "get_prop_$name";
croak "'$name' is not a known property"
unless $self->can($meth);
if ($name eq 'isa') {
if (blessed($check) && $check->isa('Test2::Compare::Wildcard')) {
# Carry forward file and lines that are set in Test2::Tools::Compare::prop.
$check = Test2::Compare::Isa->new(
input => $check->expect,
file => $check->file,
lines => $check->lines,
);
} else {
$check = Test2::Compare::Isa->new(input => $check);
}
}
push @{$self->{+ITEMS}} => [$meth, $check, $name];
}
sub deltas {
my $self = shift;
my %params = @_;
my ($got, $convert, $seen) = @params{qw/got convert seen/};
my @deltas;
my $items = $self->{+ITEMS};
for my $set (@$items) {
my ($meth, $check, $name) = @$set;
$check = $convert->($check);
my $val = $self->$meth($got);
push @deltas => $check->run(
id => [META => $name],
got => $val,
convert => $convert,
seen => $seen,
);
}
return @deltas;
}
sub get_prop_blessed { blessed($_[1]) }
sub get_prop_reftype { reftype($_[1]) }
sub get_prop_isa { $_[1] }
sub get_prop_this { $_[1] }
sub get_prop_size {
my $self = shift;
my ($it) = @_;
my $type = reftype($it) || '';
return scalar @$it if $type eq 'ARRAY';
return scalar keys %$it if $type eq 'HASH';
return undef;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Meta - Check library for meta-checks
=head1 DESCRIPTION
Sometimes in a deep comparison you want to run extra checks against an item
down the chain. This library allows you to write a check that verifies several
attributes of an item.
=head1 DEFINED CHECKS
=over 4
=item blessed
Lets you check that an item is blessed, and that it is blessed into the
expected class.
=item reftype
Lets you check the reftype of the item.
=item isa
Lets you check if the item is an instance of the expected class.
=item this
Lets you check the item itself.
=item size
Lets you check the size of the item. For an arrayref this is the number of
elements. For a hashref this is the number of keys. For everything else this is
undef.
=back
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,121 @@
package Test2::Compare::Negatable;
use strict;
use warnings;
our $VERSION = '0.000139';
require overload;
require Test2::Util::HashBase;
sub import {
my ($pkg, $file, $line) = caller;
my $sub = eval <<" EOT" or die $@;
package $pkg;
#line $line "$file"
sub { overload->import('!' => 'clone_negate', fallback => 1); Test2::Util::HashBase->import('negate')}
EOT
$sub->();
no strict 'refs';
*{"$pkg\::clone_negate"} = \&clone_negate;
*{"$pkg\::toggle_negate"} = \&toggle_negate;
}
sub clone_negate {
my $self = shift;
my $clone = $self->clone;
$clone->toggle_negate;
return $clone;
}
sub toggle_negate {
my $self = shift;
$self->set_negate($self->negate ? 0 : 1);
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Negatable - Poor mans 'role' for compare objects that can be negated.
=head1 DESCRIPTION
Using this package inside an L<Test2::Compare::Base> subclass will overload
C<!$obj> and import C<clone_negate()> and C<toggle_negate()>.
=head1 WHY?
Until perl 5.18 the 'fallback' parameter to L<overload> would not be inherited,
so we cannot use inheritance for the behavior we actually want. This module
works around the problem by emulating the C<use overload> call we want for each
consumer class.
=head1 ATTRIBUTES
=over 4
=item $bool = $obj->negate
=item $obj->set_negate($bool)
=item $attr = NEGATE()
The NEGATE attribute will be added via L<Test2::Util::HashBase>.
=back
=head1 METHODS
=over 4
=item $clone = $obj->clone_negate()
Create a shallow copy of the object, and call C<toggle_negate> on it.
=item $obj->toggle_negate()
Toggle the negate attribute. If the attribute was on it will now be off, if it
was off it will now be on.
=back
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,133 @@
package Test2::Compare::Number;
use strict;
use warnings;
use Carp qw/confess/;
use base 'Test2::Compare::Base';
our $VERSION = '0.000139';
use Test2::Util::HashBase qw/input/;
# Overloads '!' for us.
use Test2::Compare::Negatable;
sub init {
my $self = shift;
my $input = $self->{+INPUT};
confess "input must be defined for 'Number' check"
unless defined $input;
# Check for ''
confess "input must be a number for 'Number' check"
unless length($input) && $input =~ m/\S/;
$self->SUPER::init(@_);
}
sub name {
my $self = shift;
my $in = $self->{+INPUT};
return $in;
}
sub operator {
my $self = shift;
return '' unless @_;
my ($got) = @_;
return '' unless defined($got);
return '' unless length($got) && $got =~ m/\S/;
return '!=' if $self->{+NEGATE};
return '==';
}
sub verify {
my $self = shift;
my %params = @_;
my ($got, $exists) = @params{qw/got exists/};
return 0 unless $exists;
return 0 unless defined $got;
return 0 if ref $got;
return 0 unless length($got) && $got =~ m/\S/;
my $input = $self->{+INPUT};
my $negate = $self->{+NEGATE};
my @warnings;
my $out;
{
local $SIG{__WARN__} = sub { push @warnings => @_ };
$out = $negate ? ($input != $got) : ($input == $got);
}
for my $warn (@warnings) {
if ($warn =~ m/numeric/) {
$out = 0;
next; # This warning won't help anyone.
}
warn $warn;
}
return $out;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Number - Compare two values as numbers
=head1 DESCRIPTION
This is used to compare two numbers. You can also check that two numbers are not
the same.
B<Note>: This will fail if the received value is undefined. It must be a number.
B<Note>: This will fail if the comparison generates a non-numeric value warning
(which will not be shown). This is because it must get a number. The warning is
not shown as it will report to a useless line and filename. However, the test
diagnostics show both values.
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,256 @@
package Test2::Compare::Object;
use strict;
use warnings;
use Test2::Util qw/try/;
use Test2::Compare::Meta();
use base 'Test2::Compare::Base';
our $VERSION = '0.000139';
use Test2::Util::HashBase qw/calls meta refcheck ending/;
use Carp qw/croak confess/;
use Scalar::Util qw/reftype blessed/;
sub init {
my $self = shift;
$self->{+CALLS} ||= [];
$self->SUPER::init();
}
sub name { '<OBJECT>' }
sub meta_class { 'Test2::Compare::Meta' }
sub object_base { 'UNIVERSAL' }
sub verify {
my $self = shift;
my %params = @_;
my ($got, $exists) = @params{qw/got exists/};
return 0 unless $exists;
return 0 unless defined $got;
return 0 unless ref($got);
return 0 unless blessed($got);
return 0 unless $got->isa($self->object_base);
return 1;
}
sub add_prop {
my $self = shift;
$self->{+META} = $self->meta_class->new unless defined $self->{+META};
$self->{+META}->add_prop(@_);
}
sub add_field {
my $self = shift;
$self->{+REFCHECK} = Test2::Compare::Hash->new unless defined $self->{+REFCHECK};
croak "Underlying reference does not have fields"
unless $self->{+REFCHECK}->can('add_field');
$self->{+REFCHECK}->add_field(@_);
}
sub add_item {
my $self = shift;
$self->{+REFCHECK} = Test2::Compare::Array->new unless defined $self->{+REFCHECK};
croak "Underlying reference does not have items"
unless $self->{+REFCHECK}->can('add_item');
$self->{+REFCHECK}->add_item(@_);
}
sub add_call {
my $self = shift;
my ($meth, $check, $name, $context) = @_;
$name ||= ref $meth eq 'ARRAY' ? $meth->[0]
: ref $meth eq 'CODE' ? '\&CODE'
: $meth;
push @{$self->{+CALLS}} => [$meth, $check, $name, $context || 'scalar'];
}
sub deltas {
my $self = shift;
my %params = @_;
my ($got, $convert, $seen) = @params{qw/got convert seen/};
my @deltas;
my $meta = $self->{+META};
my $refcheck = $self->{+REFCHECK};
push @deltas => $meta->deltas(%params) if defined $meta;
for my $call (@{$self->{+CALLS}}) {
my ($meth, $check, $name, $context)= @$call;
$context ||= 'scalar';
$check = $convert->($check);
my @args;
if (ref($meth) eq 'ARRAY') {
($meth,@args) = @{$meth};
}
my $exists = ref($meth) || $got->can($meth);
my $val;
my ($ok, $err) = try {
$val = $exists
? ( $context eq 'list' ? [ $got->$meth(@args) ] :
$context eq 'hash' ? { $got->$meth(@args) } :
$got->$meth(@args)
)
: undef;
};
if (!$ok) {
push @deltas => $self->delta_class->new(
verified => undef,
id => [METHOD => $name],
got => undef,
check => $check,
exception => $err,
);
}
else {
push @deltas => $check->run(
id => [METHOD => $name],
convert => $convert,
seen => $seen,
exists => $exists,
$exists ? (got => $val) : (),
);
}
}
return @deltas unless defined $refcheck;
$refcheck->set_ending($self->{+ENDING});
if ($refcheck->verify(%params)) {
push @deltas => $refcheck->deltas(%params);
}
else {
push @deltas => $self->delta_class->new(
verified => undef,
id => [META => 'Object Ref'],
got => $got,
check => $refcheck,
);
}
return @deltas;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Object - Representation of an object during deep
comparison.
=head1 DESCRIPTION
This class lets you specify an expected object in a deep comparison. You can
check the fields/elements of the underlying reference, call methods to verify
results, and do meta checks for object type and ref type.
=head1 METHODS
=over 4
=item $class = $obj->meta_class
The meta-class to be used when checking the object type. This is mainly listed
because it is useful to override for specialized object subclasses.
This normally just returns L<Test2::Compare::Meta>.
=item $class = $obj->object_base
The base-class to be expected when checking the object type. This is mainly
listed because it is useful to override for specialized object subclasses.
This normally just returns 'UNIVERSAL'.
=item $obj->add_prop(...)
Add a meta-property to check, see L<Test2::Compare::Meta>. This method
just delegates.
=item $obj->add_field(...)
Add a hash-field to check, see L<Test2::Compare::Hash>. This method
just delegates.
=item $obj->add_item(...)
Add an array item to check, see L<Test2::Compare::Array>. This method
just delegates.
=item $obj->add_call($method, $check)
=item $obj->add_call($method, $check, $name)
=item $obj->add_call($method, $check, $name, $context)
Add a method call check. This will call the specified method on your object and
verify the result. C<$method> may be a method name, an array ref, or a coderef.
If it's an arrayref, the first element must be the method name, and
the rest are arguments that will be passed to it.
In the case of a coderef it can be helpful to provide an alternate
name. When no name is provided the name is either C<$method> or the
string '\&CODE'.
If C<$context> is C<'list'>, the method will be invoked in list
context, and the result will be an arrayref.
If C<$context> is C<'hash'>, the method will be invoked in list
context, and the result will be a hashref (this will warn if the
method returns an odd number of values).
=back
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,175 @@
package Test2::Compare::OrderedSubset;
use strict;
use warnings;
use base 'Test2::Compare::Base';
our $VERSION = '0.000139';
use Test2::Util::HashBase qw/inref items/;
use Carp qw/croak/;
use Scalar::Util qw/reftype/;
sub init {
my $self = shift;
if(my $ref = $self->{+INREF}) {
croak "Cannot specify both 'inref' and 'items'" if $self->{+ITEMS};
croak "'inref' must be an array reference, got '$ref'" unless reftype($ref) eq 'ARRAY';
$self->{+ITEMS} = [@{$self->{+INREF}}];
}
$self->{+ITEMS} ||= [];
$self->SUPER::init();
}
sub name { '<ORDERED SUBSET>' }
sub verify {
my $self = shift;
my %params = @_;
return 0 unless $params{exists};
defined( my $got = $params{got} ) || return 0;
return 0 unless ref($got);
return 0 unless reftype($got) eq 'ARRAY';
return 1;
}
sub add_item {
my $self = shift;
my $check = pop;
push @{$self->{+ITEMS}} => $check;
}
sub deltas {
my $self = shift;
my %params = @_;
my ($got, $convert, $seen) = @params{qw/got convert seen/};
my @deltas;
my $state = 0;
my $items = $self->{+ITEMS};
my $idx = 0;
for my $item (@$items) {
my $check = $convert->($item);
my $i = $idx;
my $found;
while($i < @$got) {
my $val = $got->[$i++];
next if $check->run(
id => [ARRAY => $i],
convert => $convert,
seen => $seen,
exists => 1,
got => $val,
);
$idx = $i;
$found++;
last;
}
next if $found;
push @deltas => Test2::Compare::Delta->new(
verified => 0,
id => ['ARRAY', '?'],
check => $check,
dne => 'got',
);
}
return @deltas;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::OrderedSubset - Internal representation of an ordered subset.
=head1 DESCRIPTION
This module is used to ensure an array has all the expected items int he
expected order. It ignores any unexpected items mixed into the array. It only
cares that all the expected values are present, and in order, everything else
is noise.
=head1 METHODS
=over 4
=item $ref = $arr->inref()
If the instance was constructed from an actual array, this will have the
reference to that array.
=item $arrayref = $arr->items()
=item $arr->set_items($arrayref)
All the expected items, in order.
=item $name = $arr->name()
Always returns the string C<< "<ORDERED SUBSET>" >>.
=item $bool = $arr->verify(got => $got, exists => $bool)
Check if C<$got> is an array reference or not.
=item $arr->add_item($item)
Add an item to the list of values to check.
=item @deltas = $arr->deltas(got => $got, convert => \&convert, seen => \%seen)
Find the differences between the expected array values and those in the C<$got>
arrayref.
=back
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,93 @@
package Test2::Compare::Pattern;
use strict;
use warnings;
use base 'Test2::Compare::Base';
our $VERSION = '0.000139';
use Test2::Util::HashBase qw/pattern stringify_got/;
# Overloads '!' for us.
use Test2::Compare::Negatable;
use Carp qw/croak/;
sub init {
my $self = shift;
croak "'pattern' is a required attribute" unless $self->{+PATTERN};
$self->{+STRINGIFY_GOT} ||= 0;
$self->SUPER::init();
}
sub name { shift->{+PATTERN} . "" }
sub operator { shift->{+NEGATE} ? '!~' : '=~' }
sub verify {
my $self = shift;
my %params = @_;
my ($got, $exists) = @params{qw/got exists/};
return 0 unless $exists;
return 0 unless defined($got);
return 0 if ref $got && !$self->stringify_got;
return $got !~ $self->{+PATTERN}
if $self->{+NEGATE};
return $got =~ $self->{+PATTERN};
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Pattern - Use a pattern to validate values in a deep
comparison.
=head1 DESCRIPTION
This allows you to use a regex to validate a value in a deep comparison.
Sometimes a value just needs to look right, it may not need to be exact. An
example is a memory address that might change from run to run.
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,109 @@
package Test2::Compare::Ref;
use strict;
use warnings;
use base 'Test2::Compare::Base';
our $VERSION = '0.000139';
use Test2::Util::HashBase qw/input/;
use Test2::Util::Ref qw/render_ref rtype/;
use Scalar::Util qw/refaddr/;
use Carp qw/croak/;
sub init {
my $self = shift;
croak "'input' is a required attribute"
unless $self->{+INPUT};
croak "'input' must be a reference, got '" . $self->{+INPUT} . "'"
unless ref $self->{+INPUT};
$self->SUPER::init();
}
sub operator { '==' }
sub name { render_ref($_[0]->{+INPUT}) }
sub verify {
my $self = shift;
my %params = @_;
my ($got, $exists) = @params{qw/got exists/};
return 0 unless $exists;
my $in = $self->{+INPUT};
return 0 unless ref $in;
return 0 unless ref $got;
my $in_type = rtype($in);
my $got_type = rtype($got);
return 0 unless $in_type eq $got_type;
# Don't let overloading mess with us.
return refaddr($in) == refaddr($got);
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Ref - Ref comparison
=head1 DESCRIPTION
Used to compare two refs in a deep comparison.
=head1 SYNOPSIS
my $ref = {};
my $check = Test2::Compare::Ref->new(input => $ref);
# Passes
is( [$ref], [$check], "The array contains the exact ref we want" );
# Fails, they both may be empty hashes, but we are looking for a specific
# reference.
is( [{}], [$check], "This will fail");
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,93 @@
package Test2::Compare::Regex;
use strict;
use warnings;
use base 'Test2::Compare::Base';
our $VERSION = '0.000139';
use Test2::Util::HashBase qw/input/;
use Test2::Util::Ref qw/render_ref rtype/;
use Carp qw/croak/;
sub init {
my $self = shift;
croak "'input' is a required attribute"
unless $self->{+INPUT};
croak "'input' must be a regex , got '" . $self->{+INPUT} . "'"
unless rtype($self->{+INPUT}) eq 'REGEXP';
$self->SUPER::init();
}
sub stringify_got { 1 }
sub operator { 'eq' }
sub name { "" . $_[0]->{+INPUT} }
sub verify {
my $self = shift;
my %params = @_;
my ($got, $exists) = @params{qw/got exists/};
return 0 unless $exists;
my $in = $self->{+INPUT};
my $got_type = rtype($got) or return 0;
return 0 unless $got_type eq 'REGEXP';
return "$in" eq "$got";
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Regex - Regex direct comparison
=head1 DESCRIPTION
Used to compare two regexes. This compares the stringified form of each regex.
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,111 @@
package Test2::Compare::Scalar;
use strict;
use warnings;
use base 'Test2::Compare::Base';
our $VERSION = '0.000139';
use Test2::Util::HashBase qw/item/;
use Carp qw/croak confess/;
use Scalar::Util qw/reftype blessed/;
sub init {
my $self = shift;
croak "'item' is a required attribute"
unless defined $self->{+ITEM};
$self->SUPER::init();
}
sub name { '<SCALAR>' }
sub operator { '${...}' }
sub verify {
my $self = shift;
my %params = @_;
my ($got, $exists) = @params{qw/got exists/};
return 0 unless $exists;
return 0 unless defined $got;
return 0 unless ref($got);
return 0 unless reftype($got) eq 'SCALAR' || reftype($got) eq 'VSTRING';
return 1;
}
sub deltas {
my $self = shift;
my %params = @_;
my ($got, $convert, $seen) = @params{qw/got convert seen/};
my $item = $self->{+ITEM};
my $check = $convert->($item);
return (
$check->run(
id => ['SCALAR' => '$*'],
got => $$got,
convert => $convert,
seen => $seen,
exists => 1,
),
);
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Scalar - Representation of a Scalar Ref in deep
comparisons
=head1 DESCRIPTION
This is used in deep comparisons to represent a scalar reference.
=head1 SYNOPSIS
my $sr = Test2::Compare::Scalar->new(item => 'foo');
is([\'foo'], $sr, "pass");
is([\'bar'], $sr, "fail, different value");
is(['foo'], $sr, "fail, not a ref");
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,153 @@
package Test2::Compare::Set;
use strict;
use warnings;
use base 'Test2::Compare::Base';
our $VERSION = '0.000139';
use Test2::Util::HashBase qw/checks _reduction/;
use Test2::Compare::Delta();
use Carp qw/croak confess/;
use Scalar::Util qw/reftype/;
sub init {
my $self = shift;
my $reduction = delete $self->{reduction} || 'any';
$self->{+CHECKS} ||= [];
$self->set_reduction($reduction);
$self->SUPER::init();
}
sub name { '<CHECK-SET>' }
sub operator { $_[0]->{+_REDUCTION} }
sub reduction { $_[0]->{+_REDUCTION} }
my %VALID = (any => 1, all => 1, none => 1);
sub set_reduction {
my $self = shift;
my ($redu) = @_;
croak "'$redu' is not a valid set reduction"
unless $VALID{$redu};
$self->{+_REDUCTION} = $redu;
}
sub verify {
my $self = shift;
my %params = @_;
return 1;
}
sub add_check {
my $self = shift;
push @{$self->{+CHECKS}} => @_;
}
sub deltas {
my $self = shift;
my %params = @_;
my $checks = $self->{+CHECKS};
my $reduction = $self->{+_REDUCTION};
my $convert = $params{convert};
unless ($checks && @$checks) {
my $file = $self->file;
my $lines = $self->lines;
my $extra = "";
if ($file and $lines and @$lines) {
my $lns = (@$lines > 1 ? 'lines ' : 'line ' ) . join ', ', @$lines;
$extra = " (Set defined in $file $lns)";
}
die "No checks defined for set$extra\n";
}
my @deltas;
my $i = 0;
for my $check (@$checks) {
my $c = $convert->($check);
my $id = [META => "Check " . $i++];
my @d = $c->run(%params, id => $id);
if ($reduction eq 'any') {
return () unless @d;
push @deltas => @d;
}
elsif ($reduction eq 'all') {
push @deltas => @d;
}
elsif ($reduction eq 'none') {
push @deltas => Test2::Compare::Delta->new(
verified => 0,
id => $id,
got => $params{got},
check => $c,
) unless @d;
}
else {
die "Invalid reduction: $reduction\n";
}
}
return @deltas;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Set - Allows a field to be matched against a set of
checks.
=head1 DESCRIPTION
This module is used by the C<check_set> function in the
L<Test2::Tools::Compare> plugin.
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,108 @@
package Test2::Compare::String;
use strict;
use warnings;
use Carp qw/confess/;
use base 'Test2::Compare::Base';
our $VERSION = '0.000139';
use Test2::Util::HashBase qw/input/;
# Overloads '!' for us.
use Test2::Compare::Negatable;
sub stringify_got { 1 }
sub init {
my $self = shift;
confess "input must be defined for 'String' check"
unless defined $self->{+INPUT};
$self->SUPER::init(@_);
}
sub name {
my $self = shift;
my $in = $self->{+INPUT};
return "$in";
}
sub operator {
my $self = shift;
return '' unless @_;
my ($got) = @_;
return '' unless defined($got);
return 'ne' if $self->{+NEGATE};
return 'eq';
}
sub verify {
my $self = shift;
my %params = @_;
my ($got, $exists) = @params{qw/got exists/};
return 0 unless $exists;
return 0 unless defined $got;
my $input = $self->{+INPUT};
my $negate = $self->{+NEGATE};
return "$input" ne "$got" if $negate;
return "$input" eq "$got";
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::String - Compare two values as strings
=head1 DESCRIPTION
This is used to compare two items after they are stringified. You can also check
that two strings are not equal.
B<Note>: This will fail if the received value is undefined, it must be defined.
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,83 @@
package Test2::Compare::Undef;
use strict;
use warnings;
use Carp qw/confess/;
use base 'Test2::Compare::Base';
our $VERSION = '0.000139';
use Test2::Util::HashBase;
# Overloads '!' for us.
use Test2::Compare::Negatable;
sub name { '<UNDEF>' }
sub operator {
my $self = shift;
return 'IS NOT' if $self->{+NEGATE};
return 'IS';
}
sub verify {
my $self = shift;
my %params = @_;
my ($got, $exists) = @params{qw/got exists/};
return 0 unless $exists;
return !defined($got) unless $self->{+NEGATE};
return defined($got);
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Undef - Check that something is undefined
=head1 DESCRIPTION
Make sure something is undefined in a comparison. You can also check that
something is defined.
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,69 @@
package Test2::Compare::Wildcard;
use strict;
use warnings;
use base 'Test2::Compare::Base';
our $VERSION = '0.000139';
use Test2::Util::HashBase qw/expect/;
use Carp qw/croak/;
sub init {
my $self = shift;
croak "'expect' is a require attribute"
unless exists $self->{+EXPECT};
$self->SUPER::init();
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Compare::Wildcard - Placeholder check.
=head1 DESCRIPTION
This module is used as a temporary placeholder for values that still need to be
converted. This is necessary to carry forward the filename and line number which
would be lost in the conversion otherwise.
=head1 SOURCE
The source code repository for Test2-Suite can be found at
F<https://github.com/Test-More/Test2-Suite/>.
=head1 MAINTAINERS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 AUTHORS
=over 4
=item Chad Granum E<lt>exodist@cpan.orgE<gt>
=back
=head1 COPYRIGHT
Copyright 2018 Chad Granum E<lt>exodist@cpan.orgE<gt>.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See F<http://dev.perl.org/licenses/>
=cut