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,176 @@
package Test2::Tools::AsyncSubtest;
use strict;
use warnings;
our $VERSION = '0.000139';
use Test2::IPC;
use Test2::AsyncSubtest;
use Test2::API qw/context/;
use Carp qw/croak/;
our @EXPORT = qw/async_subtest fork_subtest thread_subtest/;
use base 'Exporter';
sub async_subtest {
my $name = shift;
my ($params, $code);
$params = shift(@_) if @_ && ref($_[0]) eq 'HASH';
$code = shift(@_) if @_ && ref($_[0]) eq 'CODE';
my $ctx = context();
my $subtest = Test2::AsyncSubtest->new(name => $name, context => 1, hub_init_args => $params);
$subtest->run($code, $subtest) if $code;
$ctx->release;
return $subtest;
}
sub fork_subtest {
my $name = shift;
my ($params, $code);
$params = shift(@_) if @_ && ref($_[0]) eq 'HASH';
$code = shift(@_) if @_ && ref($_[0]) eq 'CODE';
my $ctx = context();
croak "fork_subtest requires a CODE reference as the second argument"
unless ref($code) eq 'CODE';
my $subtest = Test2::AsyncSubtest->new(name => $name, context => 1, hub_init_args => $params);
$subtest->run_fork($code, $subtest);
$ctx->release;
return $subtest;
}
sub thread_subtest {
my $name = shift;
my ($params, $code);
$params = shift(@_) if @_ && ref($_[0]) eq 'HASH';
$code = shift(@_) if @_ && ref($_[0]) eq 'CODE';
my $ctx = context();
croak "thread_subtest requires a CODE reference as the second argument"
unless ref($code) eq 'CODE';
my $subtest = Test2::AsyncSubtest->new(name => $name, context => 1, hub_init_args => $params);
$subtest->run_thread($code, $subtest);
$ctx->release;
return $subtest;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Tools::AsyncSubtest - Tools for writing async subtests.
=head1 DESCRIPTION
These are tools for writing async subtests. Async subtests are subtests which
can be started and stashed so that they can continue to receive events while
other events are also being generated.
=head1 SYNOPSIS
use Test2::Bundle::Extended;
use Test2::Tools::AsyncSubtest;
my $ast1 = async_subtest local => sub {
ok(1, "Inside subtest");
};
my $ast2 = fork_subtest child => sub {
ok(1, "Inside subtest in another process");
};
# You must call finish on the subtests you create. Finish will wait/join on
# any child processes and threads.
$ast1->finish;
$ast2->finish;
done_testing;
=head1 EXPORTS
Everything is exported by default.
=over 4
=item $ast = async_subtest $name
=item $ast = async_subtest $name => sub { ... }
=item $ast = async_subtest $name => \%hub_params, sub { ... }
Create an async subtest. Run the codeblock if it is provided.
=item $ast = fork_subtest $name => sub { ... }
=item $ast = fork_subtest $name => \%hub_params, sub { ... }
Create an async subtest. Run the codeblock in a forked process.
=item $ast = thread_subtest $name => sub { ... }
=item $ast = thread_subtest $name => \%hub_params, sub { ... }
B<** DISCOURAGED **> Threads are fragile. Thread tests are not even run unless
the AUTHOR_TESTING or T2_DO_THREAD_TESTS env vars are enabled.
Create an async subtest. Run the codeblock in a thread.
=back
=head1 NOTES
=over 4
=item Async Subtests are always buffered.
=back
=head1 SOURCE
The source code repository for Test2-AsyncSubtest 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>exodist7@gmail.comE<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,355 @@
package Test2::Tools::Basic;
use strict;
use warnings;
our $VERSION = '0.000139';
use Carp qw/croak/;
use Test2::API qw/context/;
our @EXPORT = qw{
ok pass fail diag note todo skip
plan skip_all done_testing bail_out
};
use base 'Exporter';
sub ok($;$@) {
my ($bool, $name, @diag) = @_;
my $ctx = context();
$ctx->ok($bool, $name, \@diag);
$ctx->release;
return $bool ? 1 : 0;
}
sub pass {
my ($name) = @_;
my $ctx = context();
$ctx->ok(1, $name);
$ctx->release;
return 1;
}
sub fail {
my ($name, @diag) = @_;
my $ctx = context();
$ctx->ok(0, $name, \@diag);
$ctx->release;
return 0;
}
sub diag {
my $ctx = context();
$ctx->diag( join '', grep { defined $_ } @_ );
$ctx->release;
}
sub note {
my $ctx = context();
$ctx->note( join '', grep { defined $_ } @_ );
$ctx->release;
}
sub todo {
my $reason = shift;
my $code = shift;
require Test2::Todo unless $INC{'Test2/Todo.pm'};
my $todo = Test2::Todo->new(reason => $reason);
return $code->() if $code;
croak "Cannot use todo() in a void context without a codeblock"
unless defined wantarray;
return $todo;
}
sub skip {
my ($why, $num) = @_;
$num ||= 1;
my $ctx = context();
$ctx->skip("skipped test", $why) for 1 .. $num;
$ctx->release;
no warnings 'exiting';
last SKIP;
}
sub plan {
my $plan = shift;
my $ctx = context();
if ($plan && $plan =~ m/[^0-9]/) {
if ($plan eq 'tests') {
$plan = shift;
}
elsif ($plan eq 'skip_all') {
skip_all(@_);
$ctx->release;
return;
}
}
$ctx->plan($plan);
$ctx->release;
}
sub skip_all {
my ($reason) = @_;
my $ctx = context();
$ctx->plan(0, SKIP => $reason);
$ctx->release if $ctx;
}
sub done_testing {
my $ctx = context();
$ctx->hub->finalize($ctx->trace, 1);
$ctx->release;
}
sub bail_out {
my ($reason) = @_;
my $ctx = context();
$ctx->bail($reason);
$ctx->release if $ctx;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Tools::Basic - Test2 implementation of the basic testing tools.
=head1 DESCRIPTION
This is a L<Test2> based implementation of the more basic tools originally
provided by L<Test::More>. Not all L<Test::More> tools are provided by this
package, only the basic/simple ones. Some tools have been modified for better
diagnostics capabilities.
=head1 SYNOPSIS
use Test2::Tools::Basic;
ok($x, "simple test");
if ($passing) {
pass('a passing test');
}
else {
fail('a failing test');
}
diag "This is a diagnostics message on STDERR";
note "This is a diagnostics message on STDOUT";
{
my $todo = todo "Reason for todo";
ok(0, "this test is todo");
}
ok(1, "this test is not todo");
todo "reason" => sub {
ok(0, "this test is todo");
};
ok(1, "this test is not todo");
SKIP: {
skip "This will wipe your drive";
# This never gets run:
ok(!system('sudo rm -rf /'), "Wipe drive");
}
done_testing;
=head1 EXPORTS
All subs are exported by default.
=head2 PLANNING
=over 4
=item plan($num)
=item plan('tests' => $num)
=item plan('skip_all' => $reason)
Set the number of tests that are expected. This must be done first or last,
never in the middle of testing.
For legacy compatibility you can specify 'tests' as the first argument before
the number. You can also use this to skip all with the 'skip_all' prefix,
followed by a reason for skipping.
=item skip_all($reason)
Set the plan to 0 with a reason, then exit true. This should be used before any
tests are run.
=item done_testing
Used to mark the end of testing. This is a safe way to have a dynamic or
unknown number of tests.
=item bail_out($reason)
Invoked when something has gone horribly wrong: stop everything, kill all threads and
processes, end the process with a false exit status.
=back
=head2 ASSERTIONS
=over 4
=item ok($bool)
=item ok($bool, $name)
=item ok($bool, $name, @diag)
Simple assertion. If C<$bool> is true the test passes, and if it is false the test
fails. The test name is optional, and all arguments after the name are added as
diagnostics message if and only if the test fails. If the test passes all the
diagnostics arguments will be ignored.
=item pass()
=item pass($name)
Fire off a passing test (a single Ok event). The name is optional
=item fail()
=item fail($name)
=item fail($name, @diag)
Fire off a failing test (a single Ok event). The name and diagnostics are optional.
=back
=head2 DIAGNOSTICS
=over 4
=item diag(@messages)
Write diagnostics messages. All items in C<@messages> will be joined into a
single string with no separator. When using TAP, diagnostics are sent to STDERR.
=item note(@messages)
Write note-diagnostics messages. All items in C<@messages> will be joined into
a single string with no separator. When using TAP, notes are sent to STDOUT.
=back
=head2 META
=over 4
=item $todo = todo($reason)
=item todo $reason => sub { ... }
This is used to mark some results as TODO. TODO means that the test may fail,
but will not cause the overall test suite to fail.
There are two ways to use this. The first is to use a codeblock, and the TODO will
only apply to the codeblock.
ok(1, "before"); # Not TODO
todo 'this will fail' => sub {
# This is TODO, as is any other test in this block.
ok(0, "blah");
};
ok(1, "after"); # Not TODO
The other way is to use a scoped variable. TODO will end when the variable is
destroyed or set to undef.
ok(1, "before"); # Not TODO
{
my $todo = todo 'this will fail';
# This is TODO, as is any other test in this block.
ok(0, "blah");
};
ok(1, "after"); # Not TODO
This is the same thing, but without the C<{...}> scope.
ok(1, "before"); # Not TODO
my $todo = todo 'this will fail';
ok(0, "blah"); # TODO
$todo = undef;
ok(1, "after"); # Not TODO
=item skip($why)
=item skip($why, $count)
This is used to skip some tests. This requires you to wrap your tests in a
block labeled C<SKIP:>. This is somewhat magical. If no C<$count> is specified
then it will issue a single result. If you specify C<$count> it will issue that
many results.
SKIP: {
skip "This will wipe your drive";
# This never gets run:
ok(!system('sudo rm -rf /'), "Wipe drive");
}
=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,193 @@
package Test2::Tools::Class;
use strict;
use warnings;
our $VERSION = '0.000139';
use Test2::API qw/context/;
use Test2::Util::Ref qw/render_ref/;
use Scalar::Util qw/blessed/;
our @EXPORT = qw/can_ok isa_ok DOES_ok/;
use base 'Exporter';
# For easier grepping
# sub isa_ok is defined here
# sub can_ok is defined here
# sub DOES_ok is defined here
BEGIN {
for my $op (qw/isa can DOES/) {
my $sub = sub($;@) {
my ($thing, @args) = @_;
my $ctx = context();
my (@items, $name);
if (ref($args[0]) eq 'ARRAY') {
$name = $args[1];
@items = @{$args[0]};
}
else {
@items = @args;
}
my $thing_name = ref($thing) ? render_ref($thing) : defined($thing) ? "$thing" : "<undef>";
$thing_name =~ s/\n/\\n/g;
$thing_name =~ s/#//g;
$thing_name =~ s/\(0x[a-f0-9]+\)//gi;
$name ||= @items == 1 ? "$thing_name\->$op('$items[0]')" : "$thing_name\->$op(...)";
unless (defined($thing) && (blessed($thing) || !ref($thing) && length($thing))) {
my $thing = defined($thing)
? ref($thing) || "'$thing'"
: '<undef>';
$ctx->ok(0, $name, ["$thing is neither a blessed reference or a package name."]);
$ctx->release;
return 0;
}
unless(UNIVERSAL->can($op) || $thing->can($op)) {
$ctx->skip($name, "'$op' is not supported on this platform");
$ctx->release;
return 1;
}
my $file = $ctx->trace->file;
my $line = $ctx->trace->line;
my @bad;
for my $item (@items) {
my ($bool, $ok, $err);
{
local ($@, $!);
$ok = eval qq/#line $line "$file"\n\$bool = \$thing->$op(\$item); 1/;
$err = $@;
}
die $err unless $ok;
next if $bool;
push @bad => $item;
}
$ctx->ok( !@bad, $name, [map { "Failed: $thing_name\->$op('$_')" } @bad]);
$ctx->release;
return !@bad;
};
no strict 'refs';
*{$op . "_ok"} = $sub;
}
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Tools::Class - Test2 implementation of the tools for testing classes.
=head1 DESCRIPTION
L<Test2> based tools for validating classes and objects. These are similar to
some tools from L<Test::More>, but they have a more consistent interface.
=head1 SYNOPSIS
use Test2::Tools::Class;
isa_ok($CLASS_OR_INSTANCE, $PARENT_CLASS1, $PARENT_CLASS2, ...);
isa_ok($CLASS_OR_INSTANCE, [$PARENT_CLASS1, $PARENT_CLASS2, ...], "Test Name");
can_ok($CLASS_OR_INSTANCE, $METHOD1, $METHOD2, ...);
can_ok($CLASS_OR_INSTANCE, [$METHOD1, $METHOD2, ...], "Test Name");
DOES_ok($CLASS_OR_INSTANCE, $ROLE1, $ROLE2, ...);
DOES_ok($CLASS_OR_INSTANCE, [$ROLE1, $ROLE2, ...], "Test Name");
=head1 EXPORTS
All subs are exported by default.
=over 4
=item can_ok($thing, @methods)
=item can_ok($thing, \@methods, $test_name)
This checks that C<$thing> (either a class name, or a blessed instance) has the
specified methods.
If the second argument is an arrayref then it will be used as the list of
methods leaving the third argument to be the test name.
=item isa_ok($thing, @classes)
=item isa_ok($thing, \@classes, $test_name)
This checks that C<$thing> (either a class name, or a blessed instance) is or
subclasses the specified classes.
If the second argument is an arrayref then it will be used as the list of
classes leaving the third argument to be the test name.
=item DOES_ok($thing, @roles)
=item DOES_ok($thing, \@roles, $test_name)
This checks that C<$thing> (either a class name, or a blessed instance) does
the specified roles.
If the second argument is an arrayref then it will be used as the list of
roles leaving the third argument to be the test name.
B<Note 1:> This uses the C<< $class->DOES(...) >> method, not the C<does()>
method Moose provides.
B<Note 2:> Not all perls have the C<DOES()> method, if you use this on those
perls the test will be skipped.
=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,514 @@
package Test2::Tools::ClassicCompare;
use strict;
use warnings;
our $VERSION = '0.000139';
our @EXPORT = qw/is is_deeply isnt like unlike cmp_ok/;
use base 'Exporter';
use Carp qw/carp/;
use Scalar::Util qw/reftype/;
use Test2::API qw/context/;
use Test2::Compare qw/compare strict_convert/;
use Test2::Util::Ref qw/rtype render_ref/;
use Test2::Util::Table qw/table/;
use Test2::Compare::Array();
use Test2::Compare::Bag();
use Test2::Compare::Custom();
use Test2::Compare::Event();
use Test2::Compare::Hash();
use Test2::Compare::Meta();
use Test2::Compare::Number();
use Test2::Compare::Object();
use Test2::Compare::OrderedSubset();
use Test2::Compare::Pattern();
use Test2::Compare::Ref();
use Test2::Compare::Regex();
use Test2::Compare::Scalar();
use Test2::Compare::Set();
use Test2::Compare::String();
use Test2::Compare::Undef();
use Test2::Compare::Wildcard();
sub is($$;$@) {
my ($got, $exp, $name, @diag) = @_;
my $ctx = context();
my @caller = caller;
my $delta = compare($got, $exp, \&is_convert);
if ($delta) {
$ctx->fail($name, $delta->diag, @diag);
}
else {
$ctx->ok(1, $name);
}
$ctx->release;
return !$delta;
}
sub isnt($$;$@) {
my ($got, $exp, $name, @diag) = @_;
my $ctx = context();
my @caller = caller;
my $delta = compare($got, $exp, \&isnt_convert);
if ($delta) {
$ctx->fail($name, $delta->diag, @diag);
}
else {
$ctx->ok(1, $name);
}
$ctx->release;
return !$delta;
}
sub is_convert {
my ($thing) = @_;
return Test2::Compare::Undef->new()
unless defined $thing;
return Test2::Compare::String->new(input => $thing);
}
sub isnt_convert {
my ($thing) = @_;
return Test2::Compare::Undef->new()
unless defined $thing;
my $str = Test2::Compare::String->new(input => $thing, negate => 1);
}
sub like($$;$@) {
my ($got, $exp, $name, @diag) = @_;
my $ctx = context();
my $delta = compare($got, $exp, \&like_convert);
if ($delta) {
$ctx->fail($name, $delta->diag, @diag);
}
else {
$ctx->ok(1, $name);
}
$ctx->release;
return !$delta;
}
sub unlike($$;$@) {
my ($got, $exp, $name, @diag) = @_;
my $ctx = context();
my $delta = compare($got, $exp, \&unlike_convert);
if ($delta) {
$ctx->fail($name, $delta->diag, @diag);
}
else {
$ctx->ok(1, $name);
}
$ctx->release;
return !$delta;
}
sub like_convert {
my ($thing) = @_;
return Test2::Compare::Pattern->new(
pattern => $thing,
stringify_got => 1,
);
}
sub unlike_convert {
my ($thing) = @_;
return Test2::Compare::Pattern->new(
negate => 1,
stringify_got => 1,
pattern => $thing,
);
}
sub is_deeply($$;$@) {
my ($got, $exp, $name, @diag) = @_;
my $ctx = context();
my @caller = caller;
my $delta = compare($got, $exp, \&strict_convert);
if ($delta) {
# Temporary thing.
my $count = 0;
my $implicit = 0;
my @deltas = ($delta);
while (my $d = shift @deltas) {
my $add = $d->children;
push @deltas => @$add if $add && @$add;
next if $d->verified;
$count++;
$implicit++ if $d->note && $d->note eq 'implicit end';
}
if ($implicit == $count) {
$ctx->ok(1, $name);
my $meth = $ENV{AUTHOR_TESTING} ? 'throw' : 'alert';
my $type = $delta->render_check;
$ctx->$meth(
join "\n",
"!!! NOTICE OF BEHAVIOR CHANGE !!!",
"This test uses at least 1 $type check without using end() or etc().",
"The exising behavior is to default to etc() when inside is_deeply().",
"The new behavior is to default to end().",
"This test will soon start to fail with the following diagnostics:",
$delta->diag,
"",
);
}
else {
$ctx->fail($name, $delta->diag, @diag);
}
}
else {
$ctx->ok(1, $name);
}
$ctx->release;
return !$delta;
}
our %OPS = (
'==' => 'num',
'!=' => 'num',
'>=' => 'num',
'<=' => 'num',
'>' => 'num',
'<' => 'num',
'<=>' => 'num',
'eq' => 'str',
'ne' => 'str',
'gt' => 'str',
'lt' => 'str',
'ge' => 'str',
'le' => 'str',
'cmp' => 'str',
'!~' => 'str',
'=~' => 'str',
'&&' => 'logic',
'||' => 'logic',
'xor' => 'logic',
'or' => 'logic',
'and' => 'logic',
'//' => 'logic',
'&' => 'bitwise',
'|' => 'bitwise',
'~~' => 'match',
);
sub cmp_ok($$$;$@) {
my ($got, $op, $exp, $name, @diag) = @_;
my $ctx = context();
# Warnings and syntax errors should report to the cmp_ok call, not the test
# context. They may not be the same.
my ($pkg, $file, $line) = caller;
my $type = $OPS{$op};
if (!$type) {
carp "operator '$op' is not supported (you can add it to %Test2::Tools::ClassicCompare::OPS)";
$type = 'unsupported';
}
local ($@, $!, $SIG{__DIE__});
my $test;
my $lived = eval <<" EOT";
#line $line "(eval in cmp_ok) $file"
\$test = (\$got $op \$exp);
1;
EOT
my $error = $@;
$ctx->send_event('Exception', error => $error) unless $lived;
if ($test && $lived) {
$ctx->ok(1, $name);
$ctx->release;
return 1;
}
# Ugh, it failed. Do roughly the same thing Test::More did to try and show
# diagnostics, but make it better by showing both the overloaded and
# unoverloaded form if overloading is in play. Also unoverload numbers,
# Test::More only unoverloaded strings.
my ($display_got, $display_exp);
if($type eq 'str') {
$display_got = defined($got) ? "$got" : undef;
$display_exp = defined($exp) ? "$exp" : undef;
}
elsif($type eq 'num') {
$display_got = defined($got) ? $got + 0 : undef;
$display_exp = defined($exp) ? $exp + 0 : undef;
}
else { # Well, we did what we could.
$display_got = $got;
$display_exp = $exp;
}
my $got_ref = ref($got) ? render_ref($got) : $got;
my $exp_ref = ref($exp) ? render_ref($exp) : $exp;
my @table;
my $show_both = (
(defined($got) && $got_ref ne "$display_got")
||
(defined($exp) && $exp_ref ne "$display_exp")
);
if ($show_both) {
@table = table(
header => ['TYPE', 'GOT', 'OP', 'CHECK'],
rows => [
[$type, $display_got, $op, $lived ? $display_exp : '<EXCEPTION>'],
['orig', $got_ref, '', $exp_ref],
],
);
}
else {
@table = table(
header => ['GOT', 'OP', 'CHECK'],
rows => [[$display_got, $op, $lived ? $display_exp : '<EXCEPTION>']],
);
}
$ctx->ok(0, $name, [join("\n", @table), @diag]);
$ctx->release;
return 0;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Tools::ClassicCompare - Classic (Test::More style) comparison tools.
=head1 DESCRIPTION
This provides comparison functions that behave like they did in L<Test::More>,
unlike the L<Test2::Tools::Compare> plugin which has modified them.
=head1 SYNOPSIS
use Test2::Tools::ClassicCompare qw/is is_deeply isnt like unlike cmp_ok/;
is($got, $expect, "These are the same when stringified");
isnt($got, $unexpect, "These are not the same when stringified");
like($got, qr/.../, "'got' matches the pattern");
unlike($got, qr/.../, "'got' does not match the pattern");
is_deeply($got, $expect, "These structures are same when checked deeply");
cmp_ok($GOT, $OP, $WANT, 'Compare these items using the specified operatr');
=head1 EXPORTS
=over 4
=item $bool = is($got, $expect)
=item $bool = is($got, $expect, $name)
=item $bool = is($got, $expect, $name, @diag)
This does a string comparison of the two arguments. If the two arguments are the
same after stringification the test passes. The test will also pass if both
arguments are undef.
The test C<$name> is optional.
The test C<@diag> is optional, it is extra diagnostics messages that will be
displayed if the test fails. The diagnostics are ignored if the test passes.
It is important to note that this tool considers C<"1"> and C<"1.0"> to not be
equal as it uses a string comparison.
See L<Test2::Tools::Compare> if you want an C<is()> function that tries
to be smarter for you.
=item $bool = isnt($got, $dont_expect)
=item $bool = isnt($got, $dont_expect, $name)
=item $bool = isnt($got, $dont_expect, $name, @diag)
This is the inverse of C<is()>, it passes when the strings are not the same.
=item $bool = like($got, $pattern)
=item $bool = like($got, $pattern, $name)
=item $bool = like($got, $pattern, $name, @diag)
Check if C<$got> matches the specified pattern. Will fail if it does not match.
The test C<$name> is optional.
The test C<@diag> is optional. It contains extra diagnostics messages that will
be displayed if the test fails. The diagnostics are ignored if the test passes.
=item $bool = unlike($got, $pattern)
=item $bool = unlike($got, $pattern, $name)
=item $bool = unlike($got, $pattern, $name, @diag)
This is the inverse of C<like()>. This will fail if C<$got> matches
C<$pattern>.
=item $bool = is_deeply($got, $expect)
=item $bool = is_deeply($got, $expect, $name)
=item $bool = is_deeply($got, $expect, $name, @diag)
This does a deep check, comparing the structures in C<$got> with those in
C<$expect>. It will recurse into hashrefs, arrayrefs, and scalar refs. All
other values will be stringified and compared as strings. It is important to
note that this tool considers C<"1"> and C<"1.0"> to not be equal as it uses a
string comparison.
This is the same as C<Test2::Tools::Compare::is()>.
=item cmp_ok($got, $op, $expect)
=item cmp_ok($got, $op, $expect, $name)
=item cmp_ok($got, $op, $expect, $name, @diag)
Compare C<$got> to C<$expect> using the operator specified in C<$op>. This is
effectively an C<eval "\$got $op \$expect"> with some other stuff to make it
more sane. This is useful for comparing numbers, overloaded objects, etc.
B<Overloading Note:> Your input is passed as-is to the comparison.
If the comparison fails between two overloaded objects, the diagnostics will
try to show you the overload form that was used in comparisons. It is possible
that the diagnostics will be wrong, though attempts have been made to improve
them since L<Test::More>.
B<Exceptions:> If the comparison results in an exception then the test will
fail and the exception will be shown.
C<cmp_ok()> has an internal list of operators it supports. If you provide an
unsupported operator it will issue a warning. You can add operators to the
C<%Test2::Tools::ClassicCompare::OPS> hash, the key should be the operator, and
the value should either be 'str' for string comparison operators, 'num' for
numeric operators, or any other true value for other operators.
Supported operators:
=over 4
=item == (num)
=item != (num)
=item >= (num)
=item <= (num)
=item > (num)
=item < (num)
=item <=> (num)
=item eq (str)
=item ne (str)
=item gt (str)
=item lt (str)
=item ge (str)
=item le (str)
=item cmp (str)
=item !~ (str)
=item =~ (str)
=item &&
=item ||
=item xor
=item or
=item and
=item //
=item &
=item |
=item ~~
=back
=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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,173 @@
package Test2::Tools::Defer;
use strict;
use warnings;
our $VERSION = '0.000139';
use Carp qw/croak/;
use Test2::Util qw/get_tid/;
use Test2::API qw{
test2_add_callback_exit
test2_pid test2_tid
};
our @EXPORT = qw/def do_def/;
use base 'Exporter';
my %TODO;
sub def {
my ($func, @args) = @_;
my @caller = caller(0);
$TODO{$caller[0]} ||= [];
push @{$TODO{$caller[0]}} => [$func, \@args, \@caller];
}
sub do_def {
my $for = caller;
my $tests = delete $TODO{$for} or croak "No tests to run!";
for my $test (@$tests) {
my ($func, $args, $caller) = @$test;
my ($pkg, $file, $line) = @$caller;
chomp(my $eval = <<" EOT");
package $pkg;
# line $line "(eval in Test2::Tools::Defer) $file"
\&$func(\@\$args);
1;
EOT
eval $eval and next;
chomp(my $error = $@);
require Data::Dumper;
chomp(my $td = Data::Dumper::Dumper($args));
$td =~ s/^\$VAR1 =/\$args: /;
die <<" EOT";
Exception: $error
--eval--
$eval
--------
Tool: $func
Caller: $caller->[0], $caller->[1], $caller->[2]
$td
EOT
}
return;
}
sub _verify {
my ($context, $exit, $new_exit) = @_;
my $not_ok = 0;
for my $pkg (keys %TODO) {
my $tests = delete $TODO{$pkg};
my $caller = $tests->[0]->[-1];
print STDOUT "not ok - deferred tests were not run!\n" unless $not_ok++;
print STDERR "# '$pkg' has deferred tests that were never run!\n";
print STDERR "# $caller->[1] at line $caller->[2]\n";
$$new_exit ||= 255;
}
}
test2_add_callback_exit(\&_verify);
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Tools::Defer - Write tests that get executed at a later time
=head1 DESCRIPTION
Sometimes you need to test things BEFORE loading the necessary functions. This
module lets you do that. You can write tests, and then have them run later,
after C<Test2> is loaded. You tell it what test function to run, and what
arguments to give it. The function name and arguments will be stored to be
executed later. When ready, run C<do_def()> to kick them off once the functions
are defined.
=head1 SYNOPSIS
use strict;
use warnings;
use Test2::Tools::Defer;
BEGIN {
def ok => (1, 'pass');
def is => ('foo', 'foo', 'runs is');
...
}
use Test2::Tools::Basic;
do_def(); # Run the tests
# Declare some more tests to run later:
def ok => (1, "another pass");
...
do_def(); # run the new tests
done_testing;
=head1 EXPORTS
=over 4
=item def function => @args;
This will store the function name, and the arguments to be run later. Note that
each package has a separate store of tests to run.
=item do_def()
This will run all the stored tests. It will also reset the list to be empty so
you can add more tests to run even later.
=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,94 @@
package Test2::Tools::Encoding;
use strict;
use warnings;
use Carp qw/croak/;
use Test2::API qw/test2_stack/;
use base 'Exporter';
our $VERSION = '0.000139';
our @EXPORT = qw/set_encoding/;
sub set_encoding {
my $enc = shift;
my $format = test2_stack->top->format;
unless ($format && eval { $format->can('encoding') }) {
$format = '<undef>' unless defined $format;
croak "Unable to set encoding on formatter '$format'";
}
$format->encoding($enc);
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Tools::Encoding - Tools for managing the encoding of L<Test2> based
tests.
=head1 DESCRIPTION
This module exports a function that lets you dynamically change the output
encoding at will.
=head1 SYNOPSIS
use Test2::Tools::Encoding;
set_encoding('utf8');
=head1 EXPORTS
All subs are exported by default.
=over 4
=item set_encoding($encoding)
This will set the encoding to whatever you specify. This will only affect the
output of the current formatter, which is usually your TAP output formatter.
=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,95 @@
package Test2::Tools::Event;
use strict;
use warnings;
our $VERSION = '0.000139';
use Test2::Util qw/pkg_to_file/;
our @EXPORT = qw/gen_event/;
use base 'Exporter';
sub gen_event {
my ($type, %fields) = @_;
$type = "Test2::Event::$type" unless $type =~ s/^\+//;
require(pkg_to_file($type));
$fields{trace} ||= Test2::Util::Trace->new(frame => [caller(0)]);
return $type->new(%fields);
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Tools::Event - Tools for generating test events.
=head1 DESCRIPTION
This module provides tools for generating events quickly by bypassing the
context/hub. This is particularly useful when testing other L<Test2> packages.
=head1 EXPORTS
=over 4
=item $e = gen_event($TYPE)
=item $e = gen_event($TYPE, %FIELDS)
=item $e = gen_event 'Ok';
=item $e = gen_event Ok => ( ... )
=item $e = gen_event '+Test2::Event::Ok' => ( ... )
This will produce an event of the specified type. C<$TYPE> is assumed to be
shorthand for C<Test2::Event::$TYPE>, you can prefix C<$TYPE> with a '+' to
drop the assumption.
An L<Test2::Util::Trace> will be generated using C<caller(0)> and will be put in
the 'trace' field of your new event, unless you specified your own 'trace'
field.
=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,163 @@
package Test2::Tools::Exception;
use strict;
use warnings;
our $VERSION = '0.000139';
use Test2::API qw/context/;
our @EXPORT = qw/dies lives try_ok/;
use base 'Exporter';
sub dies(&) {
my $code = shift;
local ($@, $!, $?);
my $ok = eval { $code->(); 1 };
my $err = $@;
return undef if $ok;
unless ($err) {
my $ctx = context();
$ctx->alert("Got exception as expected, but exception is falsy (undef, '', or 0)...");
$ctx->release;
}
return $err;
}
sub lives(&) {
my $code = shift;
my $err;
{
local ($@, $!, $?);
eval { $code->(); 1 } and return 1;
$err = $@;
}
# If the eval failed we want to set $@ to the error.
$@ = $err;
return 0;
}
sub try_ok(&;$) {
my ($code, $name) = @_;
my $ok = &lives($code);
my $err = $@;
# Context should be obtained AFTER code is run so that events inside the
# codeblock report inside the codeblock itself. This will also preserve $@
# as thrown inside the codeblock.
my $ctx = context();
chomp(my $diag = "Exception: $err");
$ctx->ok($ok, $name, [$diag]);
$ctx->release;
$@ = $err unless $ok;
return $ok;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Tools::Exception - Test2 based tools for checking exceptions
=head1 DESCRIPTION
This is the L<Test2> implementation of code used to test exceptions. This is
similar to L<Test::Fatal>, but it intentionally does much less.
=head1 SYNOPSIS
use Test2::Tools::Exception qw/dies lives/;
like(
dies { die 'xxx' },
qr/xxx/,
"Got exception"
);
ok(lives { ... }, "did not die") or note($@);
=head1 EXPORTS
All subs are exported by default.
=over 4
=item $e = dies { ... }
This will trap any exception the codeblock throws. If no exception is thrown
the sub will return undef. If an exception is thrown it will be returned. This
function preserves C<$@>, it will not be altered from its value before the sub
is called.
=item $bool = lives { ... }
This will trap any exception thrown in the codeblock. It will return true when
there is no exception, and false when there is. C<$@> is preserved from before
the sub is called when there is no exception. When an exception is trapped
C<$@> will have the exception so that you can look at it.
=item $bool = try_ok { ... }
=item $bool = try_ok { ... } "Test Description"
This will run the code block trapping any exception. If there is no exception a
passing event will be issued. If the test fails a failing event will be issued,
and the exception will be reported as diagnostics.
B<Note:> This function does not preserve C<$@> on failure, it will be set to
the exception the codeblock throws, this is by design so that you can obtain
the exception if desired.
=back
=head1 DIFFERENCES FROM TEST::FATAL
L<Test::Fatal> sets C<$Test::Builder::Level> such that failing tests inside the
exception block will report to the line where C<exception()> is called. I
disagree with this, and think the actual line of the failing test is
more important. Ultimately, though L<Test::Fatal> cannot be changed, people
probably already depend on that behavior.
=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,169 @@
package Test2::Tools::Exports;
use strict;
use warnings;
our $VERSION = '0.000139';
use Carp qw/croak carp/;
use Test2::API qw/context/;
use Test2::Util::Stash qw/get_symbol/;
our @EXPORT = qw/imported_ok not_imported_ok/;
use base 'Exporter';
sub imported_ok {
my $ctx = context();
my $caller = caller;
my @missing = grep { !get_symbol($_, $caller) } @_;
my $name = "Imported symbol";
$name .= "s" if @_ > 1;
$name .= ": ";
my $list = join(", ", @_);
substr($list, 37, length($list) - 37, '...') if length($list) > 40;
$name .= $list;
$ctx->ok(!@missing, $name, [map { "'$_' was not imported." } @missing]);
$ctx->release;
return !@missing;
}
sub not_imported_ok {
my $ctx = context();
my $caller = caller;
my @found = grep { get_symbol($_, $caller) } @_;
my $name = "Did not imported symbol";
$name .= "s" if @_ > 1;
$name .= ": ";
my $list = join(", ", @_);
substr($list, 37, length($list) - 37, '...') if length($list) > 40;
$name .= $list;
$ctx->ok(!@found, $name, [map { "'$_' was imported." } @found]);
$ctx->release;
return !@found;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Tools::Exports - Tools for validating exporters.
=head1 DESCRIPTION
These are tools for checking that symbols have been imported into your
namespace.
=head1 SYNOPSIS
use Test2::Tools::Exports
use Data::Dumper;
imported_ok qw/Dumper/;
not_imported_ok qw/dumper/;
=head1 EXPORTS
All subs are exported by default.
=over 4
=item imported_ok(@SYMBOLS)
Check that the specified symbols exist in the current package. This will not
find inherited subs. This will only find symbols in the current package's symbol
table. This B<WILL NOT> confirm that the symbols were defined outside of the
package itself.
imported_ok( '$scalar', '@array', '%hash', '&sub', 'also_a_sub' );
C<@SYMBOLS> can contain any number of symbols. Each item in the array must be a
string. The string should be the name of a symbol. If a sigil is present then
it will search for that specified type, if no sigil is specified it will be
used as a sub name.
=item not_imported_ok(@SYMBOLS)
Check that the specified symbols do not exist in the current package. This will
not find inherited subs. This will only look at symbols in the current package's
symbol table.
not_imported_ok( '$scalar', '@array', '%hash', '&sub', 'also_a_sub' );
C<@SYMBOLS> can contain any number of symbols. Each item in the array must be a
string. The string should be the name of a symbol. If a sigil is present, then
it will search for that specified type. If no sigil is specified, it will be
used as a sub name.
=back
=head1 CAVEATS
Before Perl 5.10, it is very difficult to distinguish between a package scalar
that is undeclared vs declared and undefined. Currently C<imported_ok> and
C<not_imported_ok> cannot see package scalars declared using C<our $var> unless
the variable has been assigned a defined value.
This will pass on recent perls, but fail on perls older than 5.10:
use Test2::Tools::Exports;
our $foo;
# Fails on perl onlder than 5.10
imported_ok(qw/$foo/);
If C<$foo> is imported from another module, or imported using
C<use vars qw/$foo/;> then it will work on all supported perl versions.
use Test2::Tools::Exports;
use vars qw/$foo/;
use Some::Module qw/$bar/;
# Always works
imported_ok(qw/$foo $bar/);
=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,125 @@
package Test2::Tools::GenTemp;
use strict;
use warnings;
our $VERSION = '0.000139';
use File::Temp qw/tempdir/;
use File::Spec;
our @EXPORT = qw{gen_temp};
use base 'Exporter';
sub gen_temp {
my %args = @_;
my $tempdir_args = delete $args{'-tempdir'} || [CLEANUP => 1, TMPDIR => 1];
my $tmp = tempdir(@$tempdir_args);
gen_dir($tmp, \%args);
return $tmp;
}
sub gen_dir {
my ($dir, $content) = @_;
for my $path (keys %$content) {
my $fq = File::Spec->catfile($dir, $path);
my $inside = $content->{$path};
if (ref $inside) {
# Subdirectory
mkdir($fq) or die "Could not make dir '$fq': $!";
gen_dir($fq, $inside);
}
else {
open(my $fh, '>', $fq) or die "Could not open file '$fq' for writing: $!";
print $fh $inside;
close($fh);
}
}
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Tools::GenTemp - Tool for generating a populated temp directory.
=head1 DESCRIPTION
This exports a tool that helps you make a temporary directory, nested
directories and text files within.
=head1 SYNOPSIS
use Test2::Tools::GenTemp qw/gen_temp/;
my $dir = gen_temp(
a_file => "Contents of a_file",
a_dir => {
'a_file' => 'Contents of a_dir/afile',
a_nested_dir => { ... },
},
...
);
done_testing;
=head1 EXPORTS
All subs are exported by default.
=over 4
=item gen_temp(file => 'content', subdir => [ sub_dir_file => 'content', ...], ...)
=item gen_temp(-tempdir => \@TEMPDIR_ARGS, file => 'content', subdir => [ sub_dir_file => 'content', ...], ...)
This will generate a new temporary directory with all the files and subdirs you
specify, recursively. The initial temp directory is created using
C<File::Temp::tempdir()>, you may pass arguments to tempdir using the
C<< -tempdir => [...] >> argument.
=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,124 @@
package Test2::Tools::Grab;
use strict;
use warnings;
our $VERSION = '0.000139';
use Test2::Util::Grabber;
use Test2::EventFacet::Trace();
our @EXPORT = qw/grab/;
use base 'Exporter';
sub grab { Test2::Util::Grabber->new(trace => Test2::EventFacet::Trace->new(frame => [caller(0)]) ) }
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Tools::Grab - Temporarily intercept all events without adding a scope
level.
=head1 DESCRIPTION
This package provides a function that returns an object that grabs all events.
Once the object is destroyed events will once again be sent to the main hub.
=head1 SYNOPSIS
use Test2::Tools::Grab;
my $grab = grab();
# Generate some events, they are intercepted.
ok(1, "pass");
ok(0, "fail");
my $events_a = $grab->flush;
# Generate some more events, they are intercepted.
ok(1, "pass");
ok(0, "fail");
my $events_b = $grab->finish;
=head1 EXPORTS
=over 4
=item $grab = grab()
This lets you intercept all events for a section of code without adding
anything to your call stack. This is useful for things that are sensitive to
changes in the stack depth.
my $grab = grab();
ok(1, 'foo');
ok(0, 'bar');
my $events = $grab->finish;
is(@$events, 2, "grabbed 2 events.");
If the C<$grab> object is destroyed without calling C<finish()>, it will
automatically clean up after itself and restore the parent hub.
{
my $grab = grab();
# Things are grabbed
}
# Things are back to normal
By default the hub used has C<no_ending> set to true. This will prevent the hub
from enforcing that you issued a plan and ran at least 1 test. You can turn
enforcement back one like this:
$grab->hub->set_no_ending(0);
With C<no_ending> turned off, C<finish> will run the post-test checks to
enforce the plan and that tests were run. In many cases this will result in
additional events in your events array.
=back
=head1 SEE ALSO
L<Test2::Util::Grabber> - The object constructed and returned by this tool.
=head1 SOURCE
The source code repository for Test2 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,541 @@
package Test2::Tools::Mock;
use strict;
use warnings;
use Carp qw/croak/;
use Scalar::Util qw/blessed reftype weaken/;
use Test2::Util qw/try/;
use Test2::Util::Sub qw/gen_accessor gen_reader gen_writer/;
use Test2::Mock();
use base 'Exporter';
our $VERSION = '0.000139';
our @CARP_NOT = (__PACKAGE__, 'Test2::Mock');
our @EXPORT = qw/mock mocked/;
our @EXPORT_OK = qw{
mock_obj mock_class
mock_do mock_build
mock_accessor mock_accessors
mock_getter mock_getters
mock_setter mock_setters
mock_building
};
my %HANDLERS;
my %MOCKS;
my @BUILD;
sub add_handler {
my $class = shift;
my ($for, $code) = @_;
croak "Must specify a package for the mock handler"
unless $for;
croak "Handlers must be code referneces (got: $code)"
unless $code && ref($code) eq 'CODE';
push @{$HANDLERS{$for}} => $code;
}
sub mock_building {
return unless @BUILD;
return $BUILD[-1];
}
sub mocked {
my $proto = shift;
my $class = blessed($proto) || $proto;
# Check if we have any mocks.
my $set = $MOCKS{$class} || return;
# Remove dead mocks (undef due to weaken)
pop @$set while @$set && !defined($set->[-1]);
# Remove the list if it is empty
delete $MOCKS{$class} unless @$set;
# Return the controls (may be empty list)
return @$set;
}
sub _delegate {
my ($args) = @_;
my $do = __PACKAGE__->can('mock_do');
my $obj = __PACKAGE__->can('mock_obj');
my $class = __PACKAGE__->can('mock_class');
my $build = __PACKAGE__->can('mock_build');
return $obj unless @$args;
my ($proto, $arg1) = @$args;
return $obj if ref($proto) && !blessed($proto);
if (blessed($proto)) {
return $class unless $proto->isa('Test2::Mock');
return $build if $arg1 && ref($arg1) && reftype($arg1) eq 'CODE';
}
return $class if $proto =~ m/(?:::|')/;
return $class if $proto =~ m/^_*[A-Z]/;
return $do if Test2::Mock->can($proto);
if (my $sub = __PACKAGE__->can("mock_$proto")) {
shift @$args;
return $sub;
}
return undef;
}
sub mock {
croak "undef is not a valid first argument to mock()"
if @_ && !defined($_[0]);
my $sub = _delegate(\@_);
croak "'$_[0]' does not look like a package name, and is not a valid control method"
unless $sub;
$sub->(@_);
}
sub mock_build {
my ($control, $sub) = @_;
croak "mock_build requires a Test2::Mock object as its first argument"
unless $control && blessed($control) && $control->isa('Test2::Mock');
croak "mock_build requires a coderef as its second argument"
unless $sub && ref($sub) && reftype($sub) eq 'CODE';
push @BUILD => $control;
my ($ok, $err) = &try($sub);
pop @BUILD;
die $err unless $ok;
}
sub mock_do {
my ($meth, @args) = @_;
croak "Not currently building a mock"
unless @BUILD;
my $build = $BUILD[-1];
croak "'$meth' is not a valid action for mock_do()"
if $meth =~ m/^_/ || !$build->can($meth);
$build->$meth(@args);
}
sub mock_obj {
my ($proto) = @_;
if ($proto && ref($proto) && reftype($proto) ne 'CODE') {
shift @_;
}
else {
$proto = {};
}
my $class = _generate_class();
my $control;
if (@_ == 1 && reftype($_[0]) eq 'CODE') {
my $orig = shift @_;
$control = mock_class(
$class,
sub {
my $c = mock_building;
# We want to do these BEFORE anything that the sub may do.
$c->block_load(1);
$c->purge_on_destroy(1);
$c->autoload(1);
$orig->(@_);
},
);
}
else {
$control = mock_class(
$class,
# Do these before anything the user specified.
block_load => 1,
purge_on_destroy => 1,
autoload => 1,
@_,
);
}
my $new = bless($proto, $control->class);
# We need to ensure there is a reference to the control object, and we want
# it to go away with the object.
$new->{'~~MOCK~CONTROL~~'} = $control;
return $new;
}
sub _generate_class {
my $prefix = __PACKAGE__;
for (1 .. 100) {
my $postfix = join '', map { chr(rand(26) + 65) } 1 .. 32;
my $class = $prefix . '::__TEMP__::' . $postfix;
my $file = $class;
$file =~ s{::}{/}g;
$file .= '.pm';
next if $INC{$file};
my $stash = do { no strict 'refs'; \%{"${class}\::"} };
next if keys %$stash;
return $class;
}
croak "Could not generate a unique class name after 100 attempts";
}
sub mock_class {
my $proto = shift;
my $class = blessed($proto) || $proto;
my @args = @_;
my $void = !defined(wantarray);
my $callback = sub {
my ($parent) = reverse mocked($class);
my $control;
if (@args == 1 && ref($args[0]) && reftype($args[0]) eq 'CODE') {
$control = Test2::Mock->new(class => $class);
mock_build($control, @args);
}
else {
$control = Test2::Mock->new(class => $class, @args);
}
if ($parent) {
$control->{parent} = $parent;
weaken($parent->{child} = $control);
}
$MOCKS{$class} ||= [];
push @{$MOCKS{$class}} => $control;
weaken($MOCKS{$class}->[-1]);
return $control;
};
return $callback->() unless $void;
my $level = 0;
my $caller;
while (my @call = caller($level++)) {
next if $call[0] eq __PACKAGE__;
$caller = \@call;
last;
}
my $handled;
for my $handler (@{$HANDLERS{$caller->[0]}}) {
$handled++ if $handler->(
class => $class,
caller => $caller,
builder => $callback,
args => \@args,
);
}
croak "mock_class should not be called in a void context without a registered handler"
unless $handled;
}
sub mock_accessors {
return map {( $_ => gen_accessor($_) )} @_;
}
sub mock_accessor {
my ($field) = @_;
return gen_accessor($field);
}
sub mock_getters {
my ($prefix, @list) = @_;
return map {( "$prefix$_" => gen_reader($_) )} @list;
}
sub mock_getter {
my ($field) = @_;
return gen_reader($field);
}
sub mock_setters {
my ($prefix, @list) = @_;
return map {( "$prefix$_" => gen_writer($_) )} @list;
}
sub mock_setter {
my ($field) = @_;
return gen_writer($field);
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Tools::Mock - Class/Instance mocking for Test2.
=head1 DESCRIPTION
Mocking is often an essential part of testing. This library covers some of the
most common mocking needs. This plugin is heavily influenced by L<Mock::Quick>,
but with an improved API. This plugin is also intended to play well with other
plugins in ways L<Mock::Quick> would be unable to.
=head1 SYNOPSIS
my $mock = mock 'Some::Class' => (
track => $BOOL, # Enable/Disable tracking on subs defined below
add => [
new_method => sub { ... },
],
override => [
replace_method => sub { ... },
],
set => [
replace_or_inject => sub { ... },
],
track => $bool, # enable/disable tracking again to affect mocks made after this point
..., # Argument keys may be repeated
);
Some::Class->new_method(); # Calls the newly injected method
Some::Class->replace_method(); # Calls our replacement method.
$mock->override(...) # Override some more
$mock = undef; # Undoes all the mocking, restoring all original methods.
my $simple_mock = mock {} => (
add => [
is_active => sub { ... }
]
);
$simple_mock->is_active(); # Calls our newly mocked method.
=head1 EXPORTS
=head2 DEFAULT
=over 4
=item mock
This is a one-stop shop function that delegates to one of the other methods
depending on how it is used. If you are not comfortable with a function that
has a lot of potential behaviors, you can use one of the other functions
directly.
=item @mocks = mocked($object)
=item @mocks = mocked($class)
Check if an object or class is mocked. If it is mocked the C<$mock> object(s)
(L<Test2::Mock>) will be returned.
=item $mock = mock $class => ( ... );
=item $mock = mock $instance => ( ... )
=item $mock = mock 'class', $class => ( ... )
These forms delegate to C<mock_class()> to mock a package. The third form is to
be explicit about what type of mocking you want.
=item $obj = mock()
=item $obj = mock { ... }
=item $obj = mock 'obj', ...;
These forms delegate to C<mock_obj()> to create instances of anonymous packages
where methods are vivified into existence as needed.
=item mock $mock => sub { ... }
=item mock $method => ( ... )
These forms go together, the first form will set C<$mock> as the current mock
build, then run the sub. Within the sub you can declare mock specifications
using the second form. The first form delegates to C<mock_build()>.
The second form calls the specified method on the current build. This second
form delegates to C<mock_do()>.
=back
=head2 BY REQUEST
=head3 DEFINING MOCKS
=over 4
=item $obj = mock_obj( ... )
=item $obj = mock_obj { ... } => ( ... )
=item $obj = mock_obj sub { ... }
=item $obj = mock_obj { ... } => sub { ... }
This method lets you quickly generate a blessed object. The object will be an
instance of a randomly generated package name. Methods will vivify as
read/write accessors as needed.
Arguments can be any method available to L<Test2::Mock> followed by an
argument. If the very first argument is a hashref then it will be blessed as
your new object.
If you provide a coderef instead of key/value pairs, the coderef will be run to
build the mock. (See the L</"BUILDING MOCKS"> section).
=item $mock = mock_class $class => ( ... )
=item $mock = mock_class $instance => ( ... )
=item $mock = mock_class ... => sub { ... }
This will create a new instance of L<Test2::Mock> to control the package
specified. If you give it a blessed reference it will use the class of the
instance.
Arguments can be any method available to L<Test2::Mock> followed by an
argument. If the very first argument is a hashref then it will be blessed as
your new object.
If you provide a coderef instead of key/value pairs, the coderef will be run to
build the mock. (See the L</"BUILDING MOCKS"> section).
=back
=head3 BUILDING MOCKS
=over 4
=item mock_build $mock => sub { ... }
Set C<$mock> as the current build, then run the specified code. C<$mock> will
no longer be the current build when the sub is complete.
=item $mock = mock_building()
Get the current building C<$mock> object.
=item mock_do $method => $args
Run the specified method on the currently building object.
=back
=head3 METHOD GENERATORS
=over 4
=item $sub = mock_accessor $field
Generate a read/write accessor for the specified field. This will generate a sub like the following:
$sub = sub {
my $self = shift;
($self->{$field}) = @_ if @_;
return $self->{$field};
};
=item $sub = mock_getter $field
Generate a read only accessor for the specified field. This will generate a sub like the following:
$sub = sub {
my $self = shift;
return $self->{$field};
};
=item $sub = mock_setter $field
Generate a write accessor for the specified field. This will generate a sub like the following:
$sub = sub {
my $self = shift;
($self->{$field}) = @_;
};
=item %pairs = mock_accessors(qw/name1 name2 name3/)
Generates several read/write accessors at once, returns key/value pairs where
the key is the field name, and the value is the coderef.
=item %pairs = mock_getters(qw/name1 name2 name3/)
Generates several read only accessors at once, returns key/value pairs where
the key is the field name, and the value is the coderef.
=item %pairs = mock_setters(qw/name1 name2 name3/)
Generates several write accessors at once, returns key/value pairs where the
key is the field name, and the value is the coderef.
=back
=head1 MOCK CONTROL OBJECTS
my $mock = mock(...);
Mock objects are instances of L<Test2::Mock>. See it for their methods.
=head1 SOURCE
The source code repository for Test2-Suite can be found at
L<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 L<https://dev.perl.org/licenses/>
=cut

View File

@@ -0,0 +1,173 @@
package Test2::Tools::Ref;
use strict;
use warnings;
our $VERSION = '0.000139';
use Scalar::Util qw/reftype refaddr/;
use Test2::API qw/context/;
use Test2::Util::Ref qw/render_ref/;
our @EXPORT = qw/ref_ok ref_is ref_is_not/;
use base 'Exporter';
sub ref_ok($;$$) {
my ($thing, $wanttype, $name) = @_;
my $ctx = context();
my $gotname = render_ref($thing);
my $gottype = reftype($thing);
if (!$gottype) {
$ctx->ok(0, $name, ["'$gotname' is not a reference"]);
$ctx->release;
return 0;
}
if ($wanttype && $gottype ne $wanttype) {
$ctx->ok(0, $name, ["'$gotname' is not a '$wanttype' reference"]);
$ctx->release;
return 0;
}
$ctx->ok(1, $name);
$ctx->release;
return 1;
}
sub ref_is($$;$@) {
my ($got, $exp, $name, @diag) = @_;
my $ctx = context();
$got = '<undef>' unless defined $got;
$exp = '<undef>' unless defined $exp;
my $bool = 0;
if (!ref($got)) {
$ctx->ok(0, $name, ["First argument '$got' is not a reference", @diag]);
}
elsif(!ref($exp)) {
$ctx->ok(0, $name, ["Second argument '$exp' is not a reference", @diag]);
}
else {
# Don't let overloading mess with us.
$bool = refaddr($got) == refaddr($exp);
$ctx->ok($bool, $name, ["'$got' is not the same reference as '$exp'", @diag]);
}
$ctx->release;
return $bool ? 1 : 0;
}
sub ref_is_not($$;$) {
my ($got, $exp, $name, @diag) = @_;
my $ctx = context();
$got = '<undef>' unless defined $got;
$exp = '<undef>' unless defined $exp;
my $bool = 0;
if (!ref($got)) {
$ctx->ok(0, $name, ["First argument '$got' is not a reference", @diag]);
}
elsif(!ref($exp)) {
$ctx->ok(0, $name, ["Second argument '$exp' is not a reference", @diag]);
}
else {
# Don't let overloading mess with us.
$bool = refaddr($got) != refaddr($exp);
$ctx->ok($bool, $name, ["'$got' is the same reference as '$exp'", @diag]);
}
$ctx->release;
return $bool ? 1 : 0;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Tools::Ref - Tools for validating references.
=head1 DESCRIPTION
This module contains tools that allow you to verify that something is a ref. It
also has tools to check if two refs are the same exact ref, or different. None of
the functions in this module do deep comparisons.
=head1 SYNOPSIS
use Test2::Tools::Ref;
# Ensure something is a ref.
ref_ok($ref);
# Check that $ref is a HASH reference
ref_ok($ref, 'HASH', 'Must be a hash')
ref_is($refa, $refb, "Same exact reference");
ref_is_not($refa, $refb, "Not the same exact reference");
=head1 EXPORTS
All subs are exported by default.
=over 4
=item ref_ok($thing)
=item ref_ok($thing, $type)
=item ref_ok($thing, $type, $name)
This checks that C<$thing> is a reference. If C<$type> is specified then it
will check that C<$thing> is that type of reference.
=item ref_is($ref1, $ref2, $name)
Verify that two references are the exact same reference.
=item ref_is_not($ref1, $ref2, $name)
Verify that two references are not the exact same reference.
=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,676 @@
package Test2::Tools::Spec;
use strict;
use warnings;
our $VERSION = '0.000139';
use Carp qw/croak/;
use Test2::Workflow qw/parse_args build current_build root_build init_root build_stack/;
use Test2::API qw/test2_add_callback_testing_done/;
use Test2::Workflow::Runner();
use Test2::Workflow::Task::Action();
use Test2::Workflow::Task::Group();
use Test2::Tools::Mock();
use Importer();
use vars qw/@EXPORT @EXPORT_OK/;
push @EXPORT => qw{describe cases};
push @EXPORT_OK => qw{include_workflow include_workflows spec_defaults};
my %HANDLED;
sub import {
my $class = shift;
my @caller = caller(0);
my %root_args;
my %runner_args;
my @import;
while (my $arg = shift @_) {
if ($arg =~ s/^-//) {
my $val = shift @_;
if (Test2::Workflow::Runner->can($arg)) {
$runner_args{$arg} = $val;
}
elsif (Test2::Workflow::Task::Group->can($arg)) {
$root_args{$arg} = $val;
}
elsif ($arg eq 'root_args') {
%root_args = (%root_args, %$val);
}
elsif ($arg eq 'runner_args') {
%runner_args = (%runner_args, %$val);
}
else {
croak "Unrecognized arg: $arg";
}
}
else {
push @import => $arg;
}
}
if ($HANDLED{$caller[0]}++) {
croak "Package $caller[0] has already been initialized"
if keys(%root_args) || keys(%runner_args);
}
else {
my $root = init_root(
$caller[0],
frame => \@caller,
code => sub { 1 },
%root_args,
);
my $runner = Test2::Workflow::Runner->new(%runner_args);
Test2::Tools::Mock->add_handler(
$caller[0],
sub {
my %params = @_;
my ($class, $caller, $builder, $args) = @params{qw/class caller builder args/};
my $do_it = eval "package $caller->[0];\n#line $caller->[2] \"$caller->[1]\"\nsub { \$runner\->add_mock(\$builder->()) }";
# Running
if (@{$runner->stack}) {
$do_it->();
}
else { # Not running
my $action = Test2::Workflow::Task::Action->new(
code => $do_it,
name => "mock $class",
frame => $caller,
scaffold => 1,
);
my $build = current_build() || $root;
$build->add_primary_setup($action);
$build->add_stash($builder->()) unless $build->is_root;
}
return 1;
}
);
test2_add_callback_testing_done(
sub {
return unless $root->populated;
my $g = $root->compile;
$runner->push_task($g);
$runner->run;
}
);
}
Importer->import_into($class, $caller[0], @import);
}
{
no warnings 'once';
*cases = \&describe;
*include_workflows = \&include_workflow;
}
sub describe {
my @caller = caller(0);
my $want = wantarray;
my $build = build(args => \@_, caller => \@caller, stack_stop => defined $want ? 1 : 0);
return $build if defined $want;
my $current = current_build() || root_build($caller[0])
or croak "No current workflow build!";
$current->add_primary($build);
}
sub include_workflow {
my @caller = caller(0);
my $build = current_build() || root_build(\$caller[0])
or croak "No current workflow build!";
for my $task (@_) {
croak "include_workflow only accepts Test2::Workflow::Task objects, got: $task"
unless $task->isa('Test2::Workflow::Task');
$build->add_primary($task);
}
}
sub defaults {
my %params = @_;
my ($package, $tool) = @params{qw/package tool/};
my @stack = (root_build($package), build_stack());
return unless @stack;
my %out;
for my $build (@stack) {
%out = () if $build->stack_stop;
my $new = $build->defaults->{$tool} or next;
%out = (%out, %$new);
}
return \%out;
}
# Generate a bunch of subs that only have minor differences between them.
BEGIN {
@EXPORT = qw{
tests it
case
before_all around_all after_all
before_case around_case after_case
before_each around_each after_each
};
@EXPORT_OK = qw{
mini
iso miso
async masync
};
my %stages = (
case => ['add_variant'],
tests => ['add_primary'],
it => ['add_primary'],
iso => ['add_primary'],
miso => ['add_primary'],
async => ['add_primary'],
masync => ['add_primary'],
mini => ['add_primary'],
before_all => ['add_setup'],
after_all => ['add_teardown'],
around_all => ['add_setup', 'add_teardown'],
before_case => ['add_variant_setup'],
after_case => ['add_variant_teardown'],
around_case => ['add_variant_setup', 'add_variant_teardown'],
before_each => ['add_primary_setup'],
after_each => ['add_primary_teardown'],
around_each => ['add_primary_setup', 'add_primary_teardown'],
);
my %props = (
case => [],
tests => [],
it => [],
iso => [iso => 1],
miso => [iso => 1, flat => 1],
async => [async => 1],
masync => [async => 1, flat => 1],
mini => [flat => 1],
before_all => [scaffold => 1],
after_all => [scaffold => 1],
around_all => [scaffold => 1, around => 1],
before_case => [scaffold => 1],
after_case => [scaffold => 1],
around_case => [scaffold => 1, around => 1],
before_each => [scaffold => 1],
after_each => [scaffold => 1],
around_each => [scaffold => 1, around => 1],
);
sub spec_defaults {
my ($tool, %params) = @_;
my @caller = caller(0);
croak "'$tool' is not a spec tool"
unless exists $props{$tool} || exists $stages{$tool};
my $build = current_build() || root_build($caller[0])
or croak "No current workflow build!";
my $old = $build->defaults->{$tool} ||= {};
$build->defaults->{$tool} = { %$old, %params };
}
my $run = "";
for my $func (@EXPORT, @EXPORT_OK) {
$run .= <<" EOT";
#line ${ \(__LINE__ + 1) } "${ \__FILE__ }"
sub $func {
my \@caller = caller(0);
my \$args = parse_args(args => \\\@_, caller => \\\@caller);
my \$action = Test2::Workflow::Task::Action->new(\@{\$props{$func}}, %\$args);
return \$action if defined wantarray;
my \$build = current_build() || root_build(\$caller[0])
or croak "No current workflow build!";
if (my \$defaults = defaults(package => \$caller[0], tool => '$func')) {
for my \$attr (keys \%\$defaults) {
next if defined \$action->\$attr;
my \$sub = "set_\$attr";
\$action->\$sub(\$defaults->{\$attr});
}
}
\$build->\$_(\$action) for \@{\$stages{$func}};
}
EOT
}
my ($ok, $err);
{
local $@;
$ok = eval "$run\n1";
$err = $@;
}
die $@ unless $ok;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Tools::Spec - RSPEC implementation on top of Test2::Workflow
=head1 DESCRIPTION
This uses L<Test2::Workflow> to implement an RSPEC variant. This variant
supports isolation and/or concurrency via forking or threads.
=head1 SYNOPSIS
use Test2::Bundle::Extended;
use Test2::Tools::Spec;
describe foo => sub {
before_all once => sub { ... };
before_each many => sub { ... };
after_all once => sub { ... };
after_each many => sub { ... };
case condition_a => sub { ... };
case condition_b => sub { ... };
tests foo => sub { ... };
tests bar => sub { ... };
};
done_testing;
=head1 EXPORTS
All of these use the same argument pattern. The first argument must always be a
name for the block. The last argument must always be a code reference.
Optionally a configuration hash can be inserted between the name and the code
reference.
FUNCTION "name" => sub { ... };
FUNCTION "name" => {...}, sub { ... };
=over 4
=item NAME
The first argument to a Test2::Tools::Spec function MUST be a name. The name
does not need to be unique.
=item PARAMS
This argument is optional. If present this should be a hashref.
Here are the valid keys for the hashref:
=over 8
=item flat => $bool
If this is set to true then the block will not render as a subtest, instead the
events will be inline with the parent subtest (or main test).
=item async => $bool
Set this to true to mark a block as being capable of running concurrently with
other test blocks. This does not mean the block WILL be run concurrently, just
that it can be.
=item iso => $bool
Set this to true if the block MUST be run in isolation. If this is true then
the block will run in its own forked process.
These tests will be skipped on any platform that does not have true forking, or
working/enabled threads.
Threads will ONLY be used if the T2_WORKFLOW_USE_THREADS env var is set. Thread
tests are only run if the T2_DO_THREAD_TESTS env var is set.
=item todo => $reason
Use this to mark an entire block as TODO.
=item skip => $reason
Use this to prevent a block from running at all.
=back
=item CODEREF
This argument is required. This should be a code reference that will run some
assertions.
=back
=head2 ESSENTIALS
=over 4
=item tests NAME => sub { ... }
=item tests NAME => \%params, sub { ... }
=item tests($NAME, \%PARAMS, \&CODE)
=item it NAME => sub { ... }
=item it NAME => \%params, sub { ... }
=item it($NAME, \%PARAMS, \&CODE)
This defines a test block. Test blocks are essentially subtests. All test
blocks will be run, and are expected to produce events. Test blocks can run
multiple times if the C<case()> function is also used.
C<it()> is an alias to C<tests()>.
These ARE NOT inherited by nested describe blocks.
=item case NAME => sub { ... }
=item case NAME => \%params, sub { ... }
=item case($NAME, \%PARAMS, \&CODE)
This lets you specify multiple conditions in which the test blocks should be
run. Every test block within the same group (C<describe>) will be run once per
case.
These ARE NOT inherited by nested describe blocks, but nested describe blocks
will be executed once per case.
=item before_each NAME => sub { ... }
=item before_each NAME => \%params, sub { ... }
=item before_each($NAME, \%PARAMS, \&CODE)
Specify a codeblock that should be run multiple times, once before each
C<tests()> block is run. These will run AFTER C<case()> blocks but before
C<tests()> blocks.
These ARE inherited by nested describe blocks.
=item before_case NAME => sub { ... }
=item before_case NAME => \%params, sub { ... }
=item before_case($NAME, \%PARAMS, \&CODE)
Same as C<before_each()>, except these blocks run BEFORE C<case()> blocks.
These ARE NOT inherited by nested describe blocks.
=item before_all NAME => sub { ... }
=item before_all NAME => \%params, sub { ... }
=item before_all($NAME, \%PARAMS, \&CODE)
Specify a codeblock that should be run once, before all the test blocks run.
These ARE NOT inherited by nested describe blocks.
=item around_each NAME => sub { ... }
=item around_each NAME => \%params, sub { ... }
=item around_each($NAME, \%PARAMS, \&CODE)
Specify a codeblock that should wrap around each test block. These blocks are
run AFTER case blocks, but before test blocks.
around_each wrapit => sub {
my $cont = shift;
local %ENV = ( ... );
$cont->();
...
};
The first argument to the codeblock will be a callback that MUST be called
somewhere inside the sub in order for nested items to run.
These ARE inherited by nested describe blocks.
=item around_case NAME => sub { ... }
=item around_case NAME => \%params, sub { ... }
=item around_case($NAME, \%PARAMS, \&CODE)
Same as C<around_each> except these run BEFORE case blocks.
These ARE NOT inherited by nested describe blocks.
=item around_all NAME => sub { ... }
=item around_all NAME => \%params, sub { ... }
=item around_all($NAME, \%PARAMS, \&CODE)
Same as C<around_each> except that it only runs once to wrap ALL test blocks.
These ARE NOT inherited by nested describe blocks.
=item after_each NAME => sub { ... }
=item after_each NAME => \%params, sub { ... }
=item after_each($NAME, \%PARAMS, \&CODE)
Same as C<before_each> except it runs right after each test block.
These ARE inherited by nested describe blocks.
=item after_case NAME => sub { ... }
=item after_case NAME => \%params, sub { ... }
=item after_case($NAME, \%PARAMS, \&CODE)
Same as C<after_each> except it runs right after the case block, and before the
test block.
These ARE NOT inherited by nested describe blocks.
=item after_all NAME => sub { ... }
=item after_all NAME => \%params, sub { ... }
=item after_all($NAME, \%PARAMS, \&CODE)
Same as C<before_all> except it runs after all test blocks have been run.
These ARE NOT inherited by nested describe blocks.
=back
=head2 SHORTCUTS
These are shortcuts. Each of these is the same as C<tests()> except some
parameters are added for you.
These are NOT exported by default/.
=over 4
=item mini NAME => sub { ... }
Same as:
tests NAME => { flat => 1 }, sub { ... }
=item iso NAME => sub { ... }
Same as:
tests NAME => { iso => 1 }, sub { ... }
=item miso NAME => sub { ... }
Same as:
tests NAME => { mini => 1, iso => 1 }, sub { ... }
=item async NAME => sub { ... }
Same as:
tests NAME => { async => 1 }, sub { ... }
B<Note:> This conflicts with the C<async()> exported from L<threads>. Don't
import both.
=item masync NAME => sub { ... }
Same as:
tests NAME => { minit => 1, async => 1 }, sub { ... }
=back
=head2 CUSTOM ATTRIBUTE DEFAULTS
Sometimes you want to apply default attributes to all C<tests()> or C<case()>
blocks. This can be done, and is lexical to your describe or package root!
use Test2::Bundle::Extended;
use Test2::Tools::Spec ':ALL';
# All 'tests' blocks after this declaration will have C<<iso => 1>> by default
spec_defaults tests => (iso => 1);
tests foo => sub { ... }; # isolated
tests foo, {iso => 0}, sub { ... }; # Not isolated
spec_defaults tests => (iso => 0); # Turn it off again
Defaults are inherited by nested describe blocks. You can also override the
defaults for the scope of the describe:
spec_defaults tests => (iso => 1);
describe foo => sub {
spec_defaults tests => (async => 1); # Scoped to this describe and any child describes
tests bar => sub { ... }; # both iso and async
};
tests baz => sub { ... }; # Just iso, no async.
You can apply defaults to any type of blocks:
spec_defaults case => (iso => 1); # All cases are 'iso';
Defaults are not inherited when a builder's return is captured.
spec_defaults tests => (iso => 1);
# Note we are not calling this in void context, that is the key here.
my $d = describe foo => {
tests bar => sub { ... }; # Not iso
};
=head1 EXECUTION ORDER
As each function is encountered it executes, just like any other function. The
C<describe()> function will immediately execute the codeblock it is given. All
other functions will stash their codeblocks to be run later. When
C<done_testing()> is run the workflow will be compiled, at which point all
other blocks will run.
Here is an overview of the order in which blocks get called once compiled (at
C<done_testing()>).
before_all
for-each-case {
before_case
case
after_case
# AND/OR nested describes
before_each
tests
after_each
}
after_all
=head1 SOURCE
The source code repository for Test2-Workflow 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>exodist7@gmail.comE<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,172 @@
package Test2::Tools::Subtest;
use strict;
use warnings;
our $VERSION = '0.000139';
use Test2::API qw/context run_subtest/;
use Test2::Util qw/try/;
our @EXPORT = qw/subtest_streamed subtest_buffered/;
use base 'Exporter';
sub subtest_streamed {
my $name = shift;
my $params = ref($_[0]) eq 'HASH' ? shift(@_) : {};
my $code = shift;
$params->{buffered} = 0 unless defined $params->{buffered};
my $ctx = context();
my $pass = run_subtest("Subtest: $name", $code, $params, @_);
$ctx->release;
return $pass;
}
sub subtest_buffered {
my $name = shift;
my $params = ref($_[0]) eq 'HASH' ? shift(@_) : {};
my $code = shift;
$params->{buffered} = 1 unless defined $params->{buffered};
my $ctx = context();
my $pass = run_subtest($name, $code, $params, @_);
$ctx->release;
return $pass;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Tools::Subtest - Tools for writing subtests
=head1 DESCRIPTION
This package exports subs that let you write subtests.
There are two types of subtests, buffered and streamed. Streamed subtests mimic
subtests from L<Test::More> in that they render all events as soon as they are
produced. Buffered subtests wait until the subtest completes before rendering
any results.
The main difference is that streamed subtests are unreadable when combined with
concurrency. Buffered subtests look fine with any number of concurrent threads
and processes.
=head1 SYNOPSIS
=head2 BUFFERED
use Test2::Tools::Subtest qw/subtest_buffered/;
subtest_buffered my_test => sub {
ok(1, "subtest event A");
ok(1, "subtest event B");
};
This will produce output like this:
ok 1 - my_test {
ok 1 - subtest event A
ok 2 - subtest event B
1..2
}
=head2 STREAMED
The default option is 'buffered'. If you want streamed subtests,
the way L<Test::Builder> does it, use this:
use Test2::Tools::Subtest qw/subtest_streamed/;
subtest_streamed my_test => sub {
ok(1, "subtest event A");
ok(1, "subtest event B");
};
This will produce output like this:
# Subtest: my_test
ok 1 - subtest event A
ok 2 - subtest event B
1..2
ok 1 - Subtest: my_test
=head1 IMPORTANT NOTE
You can use C<bail_out> or C<skip_all> in a subtest, but not in a BEGIN block
or C<use> statement. This is due to the way flow control works within a BEGIN
block. This is not normally an issue, but can happen in rare conditions using
eval, or script files as subtests.
=head1 EXPORTS
=over 4
=item subtest_streamed $name => $sub
=item subtest_streamed($name, $sub, @args)
=item subtest_streamed $name => \%params, $sub
=item subtest_streamed($name, \%params, $sub, @args)
Run subtest coderef, stream events as they happen.
C<\%params> is a hashref with any arguments you wish to pass into hub
construction.
=item subtest_buffered $name => $sub
=item subtest_buffered($name, $sub, @args)
=item subtest_buffered $name => \%params, $sub
=item subtest_buffered($name, \%params, $sub, @args)
Run subtest coderef, render events all at once when subtest is complete.
C<\%params> is a hashref with any arguments you wish to pass into hub
construction.
=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,118 @@
package Test2::Tools::Target;
use strict;
use warnings;
our $VERSION = '0.000139';
use Carp qw/croak/;
use Test2::Util qw/pkg_to_file/;
sub import {
my $class = shift;
my $caller = caller;
$class->import_into($caller, @_);
}
sub import_into {
my $class = shift;
my $into = shift or croak "no destination package provided";
croak "No targets specified" unless @_;
my %targets;
if (@_ == 1) {
if (ref $_[0] eq 'HASH') {
%targets = %{ $_[0] };
}
else {
($targets{CLASS}) = @_;
}
}
else {
%targets = @_;
}
for my $name (keys %targets) {
my $target = $targets{$name};
my $file = pkg_to_file($target);
require $file;
$name ||= 'CLASS';
my $const;
{
my $const_target = "$target";
$const = sub() { $const_target };
}
no strict 'refs';
*{"$into\::$name"} = \$target;
*{"$into\::$name"} = $const;
}
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Tools::Target - Alias the testing target package.
=head1 DESCRIPTION
This lets you alias the package you are testing into a constant and a package
variable.
=head1 SYNOPSIS
use Test2::Tools::Target 'Some::Package';
CLASS()->xxx; # Call 'xxx' on Some::Package
$CLASS->xxx; # Same
Or you can specify names:
use Test2::Tools::Target pkg => 'Some::Package';
pkg()->xxx; # Call 'xxx' on Some::Package
$pkg->xxx; # Same
=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,257 @@
package Test2::Tools::Tester;
use strict;
use warnings;
our $VERSION = '0.000139';
use Carp qw/croak/;
use Module::Pluggable search_path => ['Test2::EventFacet'], require => 1;
use Test2::Util::Ref qw/rtype/;
use Importer Importer => 'import';
our @EXPORT_OK = qw{
facets
filter_events
event_groups
};
my %TYPES;
for my $class (__PACKAGE__->plugins) {
my $type = $class;
$type =~ s/^Test2::EventFacet:://g;
next unless $class->isa('Test2::EventFacet');
my $key;
$key = $class->facet_key if $class->can('facet_key');
$key = lc($type) unless defined $key;
$TYPES{$type} = $class;
$TYPES{lc($type)} = $class;
$TYPES{$key} = $class;
}
sub filter_events {
my $events = shift;
my @match = map { rtype($_) eq 'REGEXP' ? $_ : qr/^\Q$_\E::/} @_;
my @out;
for my $e (@$events) {
my $trace = $e->facet_data->{trace} or next;
next unless grep { $trace->{frame}->[3] =~ $_ } @match;
push @out => $e;
}
return \@out;
}
sub event_groups {
my $events = shift;
my $out = {};
for my $e (@$events) {
my $trace = $e->facet_data->{trace};
my $tool = ($trace && $trace->{frame} && $trace->{frame}->[3]) ? $trace->{frame}->[3] : undef;
unless ($tool) {
push @{$out->{__NA__}} => $e;
next;
}
my ($pkg, $sub) = ($tool =~ m/^(.*)(?:::|')([^:']+)$/);
push @{$out->{$pkg}->{$sub}} => $e;
push @{$out->{$pkg}->{__ALL__}} => $e;
}
return $out;
}
sub facets {
my ($type, $events) = @_;
my ($key, $is_list);
my $class = $TYPES{$type};
if ($class) {
$key = $class->facet_key || lc($type);
$is_list = $class->is_list;
}
else {
$key = lc($type);
}
my @out;
for my $e (@$events) {
my $fd = $e->facet_data;
my $f = $fd->{$key} or next;
my $list = defined($is_list) ? $is_list : rtype($f) eq 'ARRAY';
if ($list) {
push @out => map { $class ? $class->new($_) : $_ } @$f;
}
else {
push @out => $class ? $class->new($f) : $f;
}
}
return \@out;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Tools::Tester - Tools to help you test other testing tools.
=head1 DESCRIPTION
This is a collection of tools that are useful when testing other test tools.
=head1 SYNOPSIS
use Test2::Tools::Tester qw/event_groups filter_events facets/;
use Test2::Tools::Basic qw/plan pass ok/;
use Test2::Tools::Compare qw/is like/;
my $events = intercept {
plan 11;
pass('pass');
ok(1, 'pass');
is(1, 1, "pass");
like(1, 1, "pass");
};
# Grab events generated by tools in Test2::Tools::Basic
my $basic = filter $events => 'Test2::Tools::Basic';
# Grab events generated by Test2::Tools::Basic;
my $compare = filter $events => 'Test2::Tools::Compare';
# Grab events generated by tools named 'ok'.
my $oks = filter $events => qr/.*::ok$/;
my $grouped = group_events $events;
# Breaks events into this structure:
{
'__NA__' => [ ... ],
'Test2::Tools::Basic' => {
'__ALL__' => [ $events->[0], $events->[1], $events->[2] ],
plan => [ $events->[0] ],
pass => [ $events->[1] ],
ok => [ $events->[2] ],
},
Test2::Tools::Compare => { ... },
}
# Get an arrayref of all the assert facets from the list of events.
my $assert_facets = facets assert => $events;
# [
# bless({ details => 'pass', pass => 1}, 'Test2::EventFacet::Assert'),
# bless({ details => 'pass', pass => 1}, 'Test2::EventFacet::Assert'),
# ]
# Same, but for info facets
my $info_facets = facets info => $events;
=head1 EXPORTS
No subs are exported by default.
=over 4
=item $array_ref = filter $events => $PACKAGE
=item $array_ref = filter $events => $PACKAGE1, $PACKAGE2
=item $array_ref = filter $events => qr/match/
=item $array_ref = filter $events => qr/match/, $PACKAGE
This function takes an arrayref of events as the first argument. All additional
arguments must either be a package name, or a regex. Any event that is
generated by a tool in any of the package, or by a tool that matches any of the
regexes, will be returned in an arrayref.
=item $grouped = group_events($events)
This function iterates all the events in the argument arrayref and splits them
into groups. The resulting data structure is:
{ PACKAGE => { SUBNAME => [ $EVENT1, $EVENT2, ... }}
If the package of an event is not known it will be put into and arrayref under
the '__NA__' key at the root of the structure. If a sub name is not known it
will typically go under the '__ANON__' key in under the package name.
In addition there is an '__ALL__' key under each package which stores all of
the events sorted into that group.
A more complete example:
{
'__NA__' => [ $event->[3] ],
'Test2::Tools::Basic' => {
'__ALL__' => [ $events->[0], $events->[1], $events->[2] ],
plan => [ $events->[0] ],
pass => [ $events->[1] ],
ok => [ $events->[2] ],
},
}
=item $arrayref = facets TYPE => $events
This function will compile a list of all facets of the specified type that are
found in the arrayref of events. If the facet has a C<Test2::EventFacet::TYPE>
package available then the facet will be constructed into an instance of the
class, otherwise it is left as a hashref. Facet Order is preserved.
my $assert_facets = facets assert => $events;
# [
# bless({ details => 'pass', pass => 1}, 'Test2::EventFacet::Assert'),
# bless({ details => 'pass', pass => 1}, 'Test2::EventFacet::Assert'),
# ]
=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,153 @@
package Test2::Tools::Warnings;
use strict;
use warnings;
our $VERSION = '0.000139';
use Test2::API qw/context/;
our @EXPORT = qw/warns warning warnings no_warnings/;
use base 'Exporter';
sub warns(&) {
my $code = shift;
my $warnings = 0;
local $SIG{__WARN__} = sub { $warnings++ };
$code->();
return $warnings;
}
sub no_warnings(&) { return !&warns(@_) }
sub warning(&) {
my $code = shift;
my @warnings;
{
local $SIG{__WARN__} = sub { push @warnings => @_ };
$code->();
return unless @warnings;
}
if (@warnings > 1) {
my $ctx = context();
$ctx->alert("Extra warnings in warning { ... }");
$ctx->note($_) for @warnings;
$ctx->release;
}
return $warnings[0];
}
sub warnings(&) {
my $code = shift;
my @warnings;
local $SIG{__WARN__} = sub { push @warnings => @_ };
$code->();
return \@warnings;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Tools::Warnings - Tools to verify warnings.
=head1 DESCRIPTION
This is a collection of tools that can be used to test code that issues
warnings.
=head1 SYNOPSIS
use Test2::Tools::Warnings qw/warns warning warnings no_warnings/;
ok(warns { warn 'a' }, "the code warns");
ok(!warns { 1 }, "The code does not warn");
is(warns { warn 'a'; warn 'b' }, 2, "got 2 warnings");
ok(no_warnings { ... }, "code did not warn");
like(
warning { warn 'xxx' },
qr/xxx/,
"Got expected warning"
);
is(
warnings { warn "a\n"; warn "b\n" },
[
"a\n",
"b\n",
],
"Got 2 specific warnings"
);
=head1 EXPORTS
All subs are exported by default.
=over 4
=item $count = warns { ... }
Returns the count of warnings produced by the block. This will always return 0,
or a positive integer.
=item $warning = warning { ... }
Returns the first warning generated by the block. If the block produces more
than one warning, they will all be shown as notes, and an actual warning will tell
you about it.
=item $warnings_ref = warnings { ... }
Returns an arrayref with all the warnings produced by the block. This will
always return an array reference. If there are no warnings, this will return an
empty array reference.
=item $bool = no_warnings { ... }
Return true if the block has no warnings. Returns false if there are warnings.
=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