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,180 @@
package Test2::API::Breakage;
use strict;
use warnings;
our $VERSION = '1.302183';
use Test2::Util qw/pkg_to_file/;
our @EXPORT_OK = qw{
upgrade_suggested
upgrade_required
known_broken
};
BEGIN { require Exporter; our @ISA = qw(Exporter) }
sub upgrade_suggested {
return (
'Test::Exception' => '0.42',
'Test::FITesque' => '0.04',
'Test::Module::Used' => '0.2.5',
'Test::Moose::More' => '0.025',
);
}
sub upgrade_required {
return (
'Test::Builder::Clutch' => '0.07',
'Test::Dist::VersionSync' => '1.1.4',
'Test::Modern' => '0.012',
'Test::SharedFork' => '0.34',
'Test::Alien' => '0.04',
'Test::UseAllModules' => '0.14',
'Test::More::Prefix' => '0.005',
'Test2::Tools::EventDumper' => 0.000007,
'Test2::Harness' => 0.000013,
'Test::DBIx::Class::Schema' => '1.0.9',
'Test::Clustericious::Cluster' => '0.30',
);
}
sub known_broken {
return (
'Net::BitTorrent' => '0.052',
'Test::Able' => '0.11',
'Test::Aggregate' => '0.373',
'Test::Flatten' => '0.11',
'Test::Group' => '0.20',
'Test::ParallelSubtest' => '0.05',
'Test::Pretty' => '0.32',
'Test::Wrapper' => '0.3.0',
'Log::Dispatch::Config::TestLog' => '0.02',
);
}
# Not reportable:
# Device::Chip => 0.07 - Tests will not pass, but not broken if already installed, also no fixed version we can upgrade to.
sub report {
my $class = shift;
my ($require) = @_;
my %suggest = __PACKAGE__->upgrade_suggested();
my %required = __PACKAGE__->upgrade_required();
my %broken = __PACKAGE__->known_broken();
my @warn;
for my $mod (keys %suggest) {
my $file = pkg_to_file($mod);
next unless $INC{$file} || ($require && eval { require $file; 1 });
my $want = $suggest{$mod};
next if eval { $mod->VERSION($want); 1 };
my $error = $@;
chomp $error;
push @warn => " * Module '$mod' is outdated, we recommed updating above $want. error was: '$error'; INC is $INC{$file}";
}
for my $mod (keys %required) {
my $file = pkg_to_file($mod);
next unless $INC{$file} || ($require && eval { require $file; 1 });
my $want = $required{$mod};
next if eval { $mod->VERSION($want); 1 };
push @warn => " * Module '$mod' is outdated and known to be broken, please update to $want or higher.";
}
for my $mod (keys %broken) {
my $file = pkg_to_file($mod);
next unless $INC{$file} || ($require && eval { require $file; 1 });
my $tested = $broken{$mod};
push @warn => " * Module '$mod' is known to be broken in version $tested and below, newer versions have not been tested. You have: " . $mod->VERSION;
}
return @warn;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::API::Breakage - What breaks at what version
=head1 DESCRIPTION
This module provides lists of modules that are broken, or have been broken in
the past, when upgrading L<Test::Builder> to use L<Test2>.
=head1 FUNCTIONS
These can be imported, or called as methods on the class.
=over 4
=item %mod_ver = upgrade_suggested()
=item %mod_ver = Test2::API::Breakage->upgrade_suggested()
This returns key/value pairs. The key is the module name, the value is the
version number. If the installed version of the module is at or below the
specified one then an upgrade would be a good idea, but not strictly necessary.
=item %mod_ver = upgrade_required()
=item %mod_ver = Test2::API::Breakage->upgrade_required()
This returns key/value pairs. The key is the module name, the value is the
version number. If the installed version of the module is at or below the
specified one then an upgrade is required for the module to work properly.
=item %mod_ver = known_broken()
=item %mod_ver = Test2::API::Breakage->known_broken()
This returns key/value pairs. The key is the module name, the value is the
version number. If the installed version of the module is at or below the
specified one then the module will not work. A newer version may work, but is
not tested or verified.
=back
=head1 SOURCE
The source code repository for Test2 can be found at
F<http://github.com/Test-More/test-more/>.
=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 2020 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,822 @@
package Test2::API::Instance;
use strict;
use warnings;
our $VERSION = '1.302183';
our @CARP_NOT = qw/Test2::API Test2::API::Instance Test2::IPC::Driver Test2::Formatter/;
use Carp qw/confess carp/;
use Scalar::Util qw/reftype/;
use Test2::Util qw/get_tid USE_THREADS CAN_FORK pkg_to_file try CAN_SIGSYS/;
use Test2::EventFacet::Trace();
use Test2::API::Stack();
use Test2::Util::HashBase qw{
_pid _tid
no_wait
finalized loaded
ipc stack formatter
contexts
add_uuid_via
-preload
ipc_disabled
ipc_polling
ipc_drivers
ipc_timeout
formatters
exit_callbacks
post_load_callbacks
context_acquire_callbacks
context_init_callbacks
context_release_callbacks
pre_subtest_callbacks
};
sub DEFAULT_IPC_TIMEOUT() { 30 }
sub pid { $_[0]->{+_PID} }
sub tid { $_[0]->{+_TID} }
# Wrap around the getters that should call _finalize.
BEGIN {
for my $finalizer (IPC, FORMATTER) {
my $orig = __PACKAGE__->can($finalizer);
my $new = sub {
my $self = shift;
$self->_finalize unless $self->{+FINALIZED};
$self->$orig;
};
no strict 'refs';
no warnings 'redefine';
*{$finalizer} = $new;
}
}
sub has_ipc { !!$_[0]->{+IPC} }
sub import {
my $class = shift;
return unless @_;
my ($ref) = @_;
$$ref = $class->new;
}
sub init { $_[0]->reset }
sub start_preload {
my $self = shift;
confess "preload cannot be started, Test2::API has already been initialized"
if $self->{+FINALIZED} || $self->{+LOADED};
return $self->{+PRELOAD} = 1;
}
sub stop_preload {
my $self = shift;
return 0 unless $self->{+PRELOAD};
$self->{+PRELOAD} = 0;
$self->post_preload_reset();
return 1;
}
sub post_preload_reset {
my $self = shift;
delete $self->{+_PID};
delete $self->{+_TID};
$self->{+ADD_UUID_VIA} = undef unless exists $self->{+ADD_UUID_VIA};
$self->{+CONTEXTS} = {};
$self->{+FORMATTERS} = [];
$self->{+FINALIZED} = undef;
$self->{+IPC} = undef;
$self->{+IPC_DISABLED} = $ENV{T2_NO_IPC} ? 1 : 0;
$self->{+IPC_TIMEOUT} = DEFAULT_IPC_TIMEOUT() unless defined $self->{+IPC_TIMEOUT};
$self->{+LOADED} = 0;
$self->{+STACK} ||= Test2::API::Stack->new;
}
sub reset {
my $self = shift;
delete $self->{+_PID};
delete $self->{+_TID};
$self->{+ADD_UUID_VIA} = undef;
$self->{+CONTEXTS} = {};
$self->{+IPC_DRIVERS} = [];
$self->{+IPC_POLLING} = undef;
$self->{+FORMATTERS} = [];
$self->{+FORMATTER} = undef;
$self->{+FINALIZED} = undef;
$self->{+IPC} = undef;
$self->{+IPC_DISABLED} = $ENV{T2_NO_IPC} ? 1 : 0;
$self->{+IPC_TIMEOUT} = DEFAULT_IPC_TIMEOUT() unless defined $self->{+IPC_TIMEOUT};
$self->{+NO_WAIT} = 0;
$self->{+LOADED} = 0;
$self->{+EXIT_CALLBACKS} = [];
$self->{+POST_LOAD_CALLBACKS} = [];
$self->{+CONTEXT_ACQUIRE_CALLBACKS} = [];
$self->{+CONTEXT_INIT_CALLBACKS} = [];
$self->{+CONTEXT_RELEASE_CALLBACKS} = [];
$self->{+PRE_SUBTEST_CALLBACKS} = [];
$self->{+STACK} = Test2::API::Stack->new;
}
sub _finalize {
my $self = shift;
my ($caller) = @_;
$caller ||= [caller(1)];
confess "Attempt to initialize Test2::API during preload"
if $self->{+PRELOAD};
$self->{+FINALIZED} = $caller;
$self->{+_PID} = $$ unless defined $self->{+_PID};
$self->{+_TID} = get_tid() unless defined $self->{+_TID};
unless ($self->{+FORMATTER}) {
my ($formatter, $source);
if ($ENV{T2_FORMATTER}) {
$source = "set by the 'T2_FORMATTER' environment variable";
if ($ENV{T2_FORMATTER} =~ m/^(\+)?(.*)$/) {
$formatter = $1 ? $2 : "Test2::Formatter::$2"
}
else {
$formatter = '';
}
}
elsif (@{$self->{+FORMATTERS}}) {
($formatter) = @{$self->{+FORMATTERS}};
$source = "Most recently added";
}
else {
$formatter = 'Test2::Formatter::TAP';
$source = 'default formatter';
}
unless (ref($formatter) || $formatter->can('write')) {
my $file = pkg_to_file($formatter);
my ($ok, $err) = try { require $file };
unless ($ok) {
my $line = "* COULD NOT LOAD FORMATTER '$formatter' ($source) *";
my $border = '*' x length($line);
die "\n\n $border\n $line\n $border\n\n$err";
}
}
$self->{+FORMATTER} = $formatter;
}
# Turn on IPC if threads are on, drivers are registered, or the Test2::IPC
# module is loaded.
return if $self->{+IPC_DISABLED};
return unless USE_THREADS || $INC{'Test2/IPC.pm'} || @{$self->{+IPC_DRIVERS}};
# Turn on polling by default, people expect it.
$self->enable_ipc_polling;
unless (@{$self->{+IPC_DRIVERS}}) {
my ($ok, $error) = try { require Test2::IPC::Driver::Files };
die $error unless $ok;
push @{$self->{+IPC_DRIVERS}} => 'Test2::IPC::Driver::Files';
}
for my $driver (@{$self->{+IPC_DRIVERS}}) {
next unless $driver->can('is_viable') && $driver->is_viable;
$self->{+IPC} = $driver->new or next;
return;
}
die "IPC has been requested, but no viable drivers were found. Aborting...\n";
}
sub formatter_set { $_[0]->{+FORMATTER} ? 1 : 0 }
sub add_formatter {
my $self = shift;
my ($formatter) = @_;
unshift @{$self->{+FORMATTERS}} => $formatter;
return unless $self->{+FINALIZED};
# Why is the @CARP_NOT entry not enough?
local %Carp::Internal = %Carp::Internal;
$Carp::Internal{'Test2::Formatter'} = 1;
carp "Formatter $formatter loaded too late to be used as the global formatter";
}
sub add_context_acquire_callback {
my $self = shift;
my ($code) = @_;
my $rtype = reftype($code) || "";
confess "Context-acquire callbacks must be coderefs"
unless $code && $rtype eq 'CODE';
push @{$self->{+CONTEXT_ACQUIRE_CALLBACKS}} => $code;
}
sub add_context_init_callback {
my $self = shift;
my ($code) = @_;
my $rtype = reftype($code) || "";
confess "Context-init callbacks must be coderefs"
unless $code && $rtype eq 'CODE';
push @{$self->{+CONTEXT_INIT_CALLBACKS}} => $code;
}
sub add_context_release_callback {
my $self = shift;
my ($code) = @_;
my $rtype = reftype($code) || "";
confess "Context-release callbacks must be coderefs"
unless $code && $rtype eq 'CODE';
push @{$self->{+CONTEXT_RELEASE_CALLBACKS}} => $code;
}
sub add_post_load_callback {
my $self = shift;
my ($code) = @_;
my $rtype = reftype($code) || "";
confess "Post-load callbacks must be coderefs"
unless $code && $rtype eq 'CODE';
push @{$self->{+POST_LOAD_CALLBACKS}} => $code;
$code->() if $self->{+LOADED};
}
sub add_pre_subtest_callback {
my $self = shift;
my ($code) = @_;
my $rtype = reftype($code) || "";
confess "Pre-subtest callbacks must be coderefs"
unless $code && $rtype eq 'CODE';
push @{$self->{+PRE_SUBTEST_CALLBACKS}} => $code;
}
sub load {
my $self = shift;
unless ($self->{+LOADED}) {
confess "Attempt to initialize Test2::API during preload"
if $self->{+PRELOAD};
$self->{+_PID} = $$ unless defined $self->{+_PID};
$self->{+_TID} = get_tid() unless defined $self->{+_TID};
# This is for https://github.com/Test-More/test-more/issues/16
# and https://rt.perl.org/Public/Bug/Display.html?id=127774
# END blocks run in reverse order. This insures the END block is loaded
# as late as possible. It will not solve all cases, but it helps.
eval "END { Test2::API::test2_set_is_end() }; 1" or die $@;
$self->{+LOADED} = 1;
$_->() for @{$self->{+POST_LOAD_CALLBACKS}};
}
return $self->{+LOADED};
}
sub add_exit_callback {
my $self = shift;
my ($code) = @_;
my $rtype = reftype($code) || "";
confess "End callbacks must be coderefs"
unless $code && $rtype eq 'CODE';
push @{$self->{+EXIT_CALLBACKS}} => $code;
}
sub ipc_disable {
my $self = shift;
confess "Attempt to disable IPC after it has been initialized"
if $self->{+IPC};
$self->{+IPC_DISABLED} = 1;
}
sub add_ipc_driver {
my $self = shift;
my ($driver) = @_;
unshift @{$self->{+IPC_DRIVERS}} => $driver;
return unless $self->{+FINALIZED};
# Why is the @CARP_NOT entry not enough?
local %Carp::Internal = %Carp::Internal;
$Carp::Internal{'Test2::IPC::Driver'} = 1;
carp "IPC driver $driver loaded too late to be used as the global ipc driver";
}
sub enable_ipc_polling {
my $self = shift;
$self->{+_PID} = $$ unless defined $self->{+_PID};
$self->{+_TID} = get_tid() unless defined $self->{+_TID};
$self->add_context_init_callback(
# This is called every time a context is created, it needs to be fast.
# $_[0] is a context object
sub {
return unless $self->{+IPC_POLLING};
return unless $self->{+IPC};
return unless $self->{+IPC}->pending();
return $_[0]->{hub}->cull;
}
) unless defined $self->ipc_polling;
$self->set_ipc_polling(1);
}
sub get_ipc_pending {
my $self = shift;
return -1 unless $self->{+IPC};
$self->{+IPC}->pending();
}
sub _check_pid {
my $self = shift;
my ($pid) = @_;
return kill(0, $pid);
}
sub set_ipc_pending {
my $self = shift;
return unless $self->{+IPC};
my ($val) = @_;
confess "value is required for set_ipc_pending"
unless $val;
$self->{+IPC}->set_pending($val);
}
sub disable_ipc_polling {
my $self = shift;
return unless defined $self->{+IPC_POLLING};
$self->{+IPC_POLLING} = 0;
}
sub _ipc_wait {
my ($timeout) = @_;
my $fail = 0;
$timeout = DEFAULT_IPC_TIMEOUT() unless defined $timeout;
my $ok = eval {
if (CAN_FORK) {
local $SIG{ALRM} = sub { die "Timeout waiting on child processes" };
alarm $timeout;
while (1) {
my $pid = CORE::wait();
my $err = $?;
last if $pid == -1;
next unless $err;
$fail++;
my $sig = $err & 127;
my $exit = $err >> 8;
warn "Process $pid did not exit cleanly (wstat: $err, exit: $exit, sig: $sig)\n";
}
alarm 0;
}
if (USE_THREADS) {
my $start = time;
while (1) {
last unless threads->list();
die "Timeout waiting on child thread" if time - $start >= $timeout;
sleep 1;
for my $t (threads->list) {
# threads older than 1.34 do not have this :-(
next if $t->can('is_joinable') && !$t->is_joinable;
$t->join;
# In older threads we cannot check if a thread had an error unless
# we control it and its return.
my $err = $t->can('error') ? $t->error : undef;
next unless $err;
my $tid = $t->tid();
$fail++;
chomp($err);
warn "Thread $tid did not end cleanly: $err\n";
}
}
}
1;
};
my $error = $@;
return 0 if $ok && !$fail;
warn $error unless $ok;
return 255;
}
sub set_exit {
my $self = shift;
return if $self->{+PRELOAD};
my $exit = $?;
my $new_exit = $exit;
if ($INC{'Test/Builder.pm'} && $Test::Builder::VERSION ne $Test2::API::VERSION) {
print STDERR <<" EOT";
********************************************************************************
* *
* Test::Builder -- Test2::API version mismatch detected *
* *
********************************************************************************
Test2::API Version: $Test2::API::VERSION
Test::Builder Version: $Test::Builder::VERSION
This is not a supported configuration, you will have problems.
EOT
}
for my $ctx (values %{$self->{+CONTEXTS}}) {
next unless $ctx;
next if $ctx->_aborted && ${$ctx->_aborted};
# Only worry about contexts in this PID
my $trace = $ctx->trace || next;
next unless $trace->pid && $trace->pid == $$;
# Do not worry about contexts that have no hub
my $hub = $ctx->hub || next;
# Do not worry if the state came to a sudden end.
next if $hub->bailed_out;
next if defined $hub->skip_reason;
# now we worry
$trace->alert("context object was never released! This means a testing tool is behaving very badly");
$exit = 255;
$new_exit = 255;
}
if (!defined($self->{+_PID}) or !defined($self->{+_TID}) or $self->{+_PID} != $$ or $self->{+_TID} != get_tid()) {
$? = $exit;
return;
}
my @hubs = $self->{+STACK} ? $self->{+STACK}->all : ();
if (@hubs and $self->{+IPC} and !$self->{+NO_WAIT}) {
local $?;
my %seen;
for my $hub (reverse @hubs) {
my $ipc = $hub->ipc or next;
next if $seen{$ipc}++;
$ipc->waiting();
}
my $ipc_exit = _ipc_wait($self->{+IPC_TIMEOUT});
$new_exit ||= $ipc_exit;
}
# None of this is necessary if we never got a root hub
if(my $root = shift @hubs) {
my $trace = Test2::EventFacet::Trace->new(
frame => [__PACKAGE__, __FILE__, 0, __PACKAGE__ . '::END'],
detail => __PACKAGE__ . ' END Block finalization',
);
my $ctx = Test2::API::Context->new(
trace => $trace,
hub => $root,
);
if (@hubs) {
$ctx->diag("Test ended with extra hubs on the stack!");
$new_exit = 255;
}
unless ($root->no_ending) {
local $?;
$root->finalize($trace) unless $root->ended;
$_->($ctx, $exit, \$new_exit) for @{$self->{+EXIT_CALLBACKS}};
$new_exit ||= $root->failed;
$new_exit ||= 255 unless $root->is_passing;
}
}
$new_exit = 255 if $new_exit > 255;
if ($new_exit && eval { require Test2::API::Breakage; 1 }) {
my @warn = Test2::API::Breakage->report();
if (@warn) {
print STDERR "\nYou have loaded versions of test modules known to have problems with Test2.\nThis could explain some test failures.\n";
print STDERR "$_\n" for @warn;
print STDERR "\n";
}
}
$? = $new_exit;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::API::Instance - Object used by Test2::API under the hood
=head1 DESCRIPTION
This object encapsulates the global shared state tracked by
L<Test2>. A single global instance of this package is stored (and
obscured) by the L<Test2::API> package.
There is no reason to directly use this package. This package is documented for
completeness. This package can change, or go away completely at any time.
Directly using, or monkeypatching this package is not supported in any way
shape or form.
=head1 SYNOPSIS
use Test2::API::Instance;
my $obj = Test2::API::Instance->new;
=over 4
=item $pid = $obj->pid
PID of this instance.
=item $obj->tid
Thread ID of this instance.
=item $obj->reset()
Reset the object to defaults.
=item $obj->load()
Set the internal state to loaded, and run and stored post-load callbacks.
=item $bool = $obj->loaded
Check if the state is set to loaded.
=item $arrayref = $obj->post_load_callbacks
Get the post-load callbacks.
=item $obj->add_post_load_callback(sub { ... })
Add a post-load callback. If C<load()> has already been called then the callback will
be immediately executed. If C<load()> has not been called then the callback will be
stored and executed later when C<load()> is called.
=item $hashref = $obj->contexts()
Get a hashref of all active contexts keyed by hub id.
=item $arrayref = $obj->context_acquire_callbacks
Get all context acquire callbacks.
=item $arrayref = $obj->context_init_callbacks
Get all context init callbacks.
=item $arrayref = $obj->context_release_callbacks
Get all context release callbacks.
=item $arrayref = $obj->pre_subtest_callbacks
Get all pre-subtest callbacks.
=item $obj->add_context_init_callback(sub { ... })
Add a context init callback. Subs are called every time a context is created. Subs
get the newly created context as their only argument.
=item $obj->add_context_release_callback(sub { ... })
Add a context release callback. Subs are called every time a context is released. Subs
get the released context as their only argument. These callbacks should not
call release on the context.
=item $obj->add_pre_subtest_callback(sub { ... })
Add a pre-subtest callback. Subs are called every time a subtest is
going to be run. Subs get the subtest name, coderef, and any
arguments.
=item $obj->set_exit()
This is intended to be called in an C<END { ... }> block. This will look at
test state and set $?. This will also call any end callbacks, and wait on child
processes/threads.
=item $obj->set_ipc_pending($val)
Tell other processes and threads there is a pending event. C<$val> should be a
unique value no other thread/process will generate.
B<Note:> This will also make the current process see a pending event.
=item $pending = $obj->get_ipc_pending()
This returns -1 if it is not possible to know.
This returns 0 if there are no pending events.
This returns 1 if there are pending events.
=item $timeout = $obj->ipc_timeout;
=item $obj->set_ipc_timeout($timeout);
How long to wait for child processes and threads before aborting.
=item $drivers = $obj->ipc_drivers
Get the list of IPC drivers.
=item $obj->add_ipc_driver($DRIVER_CLASS)
Add an IPC driver to the list. The most recently added IPC driver will become
the global one during initialization. If a driver is added after initialization
has occurred a warning will be generated:
"IPC driver $driver loaded too late to be used as the global ipc driver"
=item $bool = $obj->ipc_polling
Check if polling is enabled.
=item $obj->enable_ipc_polling
Turn on polling. This will cull events from other processes and threads every
time a context is created.
=item $obj->disable_ipc_polling
Turn off IPC polling.
=item $bool = $obj->no_wait
=item $bool = $obj->set_no_wait($bool)
Get/Set no_wait. This option is used to turn off process/thread waiting at exit.
=item $arrayref = $obj->exit_callbacks
Get the exit callbacks.
=item $obj->add_exit_callback(sub { ... })
Add an exit callback. This callback will be called by C<set_exit()>.
=item $bool = $obj->finalized
Check if the object is finalized. Finalization happens when either C<ipc()>,
C<stack()>, or C<format()> are called on the object. Once finalization happens
these fields are considered unchangeable (not enforced here, enforced by
L<Test2>).
=item $ipc = $obj->ipc
Get the one true IPC instance.
=item $obj->ipc_disable
Turn IPC off
=item $bool = $obj->ipc_disabled
Check if IPC is disabled
=item $stack = $obj->stack
Get the one true hub stack.
=item $formatter = $obj->formatter
Get the global formatter. By default this is the C<'Test2::Formatter::TAP'>
package. This could be any package that implements the C<write()> method. This
can also be an instantiated object.
=item $bool = $obj->formatter_set()
Check if a formatter has been set.
=item $obj->add_formatter($class)
=item $obj->add_formatter($obj)
Add a formatter. The most recently added formatter will become the global one
during initialization. If a formatter is added after initialization has occurred
a warning will be generated:
"Formatter $formatter loaded too late to be used as the global formatter"
=item $obj->set_add_uuid_via(sub { ... })
=item $sub = $obj->add_uuid_via()
This allows you to provide a UUID generator. If provided UUIDs will be attached
to all events, hubs, and contexts. This is useful for storing, tracking, and
linking these objects.
The sub you provide should always return a unique identifier. Most things will
expect a proper UUID string, however nothing in Test2::API enforces this.
The sub will receive exactly 1 argument, the type of thing being tagged
'context', 'hub', or 'event'. In the future additional things may be tagged, in
which case new strings will be passed in. These are purely informative, you can
(and usually should) ignore them.
=back
=head1 SOURCE
The source code repository for Test2 can be found at
F<http://github.com/Test-More/test-more/>.
=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 2020 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,634 @@
package Test2::API::InterceptResult;
use strict;
use warnings;
our $VERSION = '1.302183';
use Scalar::Util qw/blessed/;
use Test2::Util qw/pkg_to_file/;
use Storable qw/dclone/;
use Carp qw/croak/;
use Test2::API::InterceptResult::Squasher;
use Test2::API::InterceptResult::Event;
use Test2::API::InterceptResult::Hub;
sub new {
croak "Called a method that creates a new instance in void context" unless defined wantarray;
my $class = shift;
bless([@_], $class);
}
sub new_from_ref {
croak "Called a method that creates a new instance in void context" unless defined wantarray;
bless($_[1], $_[0]);
}
sub clone { blessed($_[0])->new(@{dclone($_[0])}) }
sub event_list { @{$_[0]} }
sub _upgrade {
my $self = shift;
my ($event, %params) = @_;
my $blessed = blessed($event);
my $upgrade_class = $params{upgrade_class} ||= 'Test2::API::InterceptResult::Event';
return $event if $blessed && $event->isa($upgrade_class) && !$params{_upgrade_clone};
my $fd = dclone($blessed ? $event->facet_data : $event);
my $class = $params{result_class} ||= blessed($self);
if (my $parent = $fd->{parent}) {
$parent->{children} = $class->new_from_ref($parent->{children} || [])->upgrade(%params);
}
my $uc_file = pkg_to_file($upgrade_class);
require($uc_file) unless $INC{$uc_file};
return $upgrade_class->new(facet_data => $fd, result_class => $class);
}
sub hub {
my $self = shift;
my $hub = Test2::API::InterceptResult::Hub->new();
$hub->process($_) for @$self;
$hub->set_ended(1);
return $hub;
}
sub state {
my $self = shift;
my %params = @_;
my $hub = $self->hub;
my $out = {
map {($_ => scalar $hub->$_)} qw/count failed is_passing plan bailed_out skip_reason/
};
$out->{bailed_out} = $self->_upgrade($out->{bailed_out}, %params)->bailout_reason || 1
if $out->{bailed_out};
$out->{follows_plan} = $hub->check_plan;
return $out;
}
sub upgrade {
my $self = shift;
my %params = @_;
my @out = map { $self->_upgrade($_, %params, _upgrade_clone => 1) } @$self;
return blessed($self)->new_from_ref(\@out)
unless $params{in_place};
@$self = @out;
return $self;
}
sub squash_info {
my $self = shift;
my %params = @_;
my @out;
{
my $squasher = Test2::API::InterceptResult::Squasher->new(events => \@out);
# Clone to make sure we do not indirectly modify an existing one if it
# is already upgraded
$squasher->process($self->_upgrade($_, %params)->clone) for @$self;
$squasher->flush_down();
}
return blessed($self)->new_from_ref(\@out)
unless $params{in_place};
@$self = @out;
return $self;
}
sub asserts { shift->grep(has_assert => @_) }
sub subtests { shift->grep(has_subtest => @_) }
sub diags { shift->grep(has_diags => @_) }
sub notes { shift->grep(has_notes => @_) }
sub errors { shift->grep(has_errors => @_) }
sub plans { shift->grep(has_plan => @_) }
sub causes_fail { shift->grep(causes_fail => @_) }
sub causes_failure { shift->grep(causes_failure => @_) }
sub flatten { shift->map(flatten => @_) }
sub briefs { shift->map(brief => @_) }
sub summaries { shift->map(summary => @_) }
sub subtest_results { shift->map(subtest_result => @_) }
sub diag_messages { shift->map(diag_messages => @_) }
sub note_messages { shift->map(note_messages => @_) }
sub error_messages { shift->map(error_messages => @_) }
no warnings 'once';
*map = sub {
my $self = shift;
my ($call, %params) = @_;
my $args = $params{args} ||= [];
return [map { local $_ = $self->_upgrade($_, %params); $_->$call(@$args) } @$self];
};
*grep = sub {
my $self = shift;
my ($call, %params) = @_;
my $args = $params{args} ||= [];
my @out = grep { local $_ = $self->_upgrade($_, %params); $_->$call(@$args) } @$self;
return blessed($self)->new_from_ref(\@out)
unless $params{in_place};
@$self = @out;
return $self;
};
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::API::InterceptResult - Representation of a list of events.
=head1 DESCRIPTION
This class represents a list of events, normally obtained using C<intercept()>
from L<Test2::API>.
This class is intended for people who with to verify the results of test tools
they write.
This class provides methods to normalize, summarize, or map the list of events.
The output of these operations makes verifying your testing tools and the
events they generate significantly easier. In most cases this spares you from
needing a deep understanding of the event/facet model.
=head1 SYNOPSIS
Usually you get an instance of this class when you use C<intercept()> from
L<Test2::API>.
use Test2::V0;
use Test2::API qw/intercept/;
my $events = intercept {
ok(1, "pass");
ok(0, "fail");
todo "broken" => sub { ok(0, "fixme") };
plan 3;
};
# This is typically the most useful construct
# squash_info() merges assertions and diagnostics that are associated
# (and returns a new instance with the modifications)
# flatten() condenses the facet data into the key details for each event
# (and returns those structures in an arrayref)
is(
$events->squash_info->flatten(),
[
{
causes_failure => 0,
name => 'pass',
pass => 1,
trace_file => 'xxx.t',
trace_line => 5,
},
{
causes_failure => 1,
name => 'fail',
pass => 0,
trace_file => 'xxx.t',
trace_line => 6,
# There can be more than one diagnostics message so this is
# always an array when present.
diag => ["Failed test 'fail'\nat xxx.t line 6."],
},
{
causes_failure => 0,
name => 'fixme',
pass => 0,
trace_file => 'xxx.t',
trace_line => 7,
# There can be more than one diagnostics message or todo
# reason, so these are always an array when present.
todo => ['broken'],
# Diag message was turned into a note since the assertion was
# TODO
note => ["Failed test 'fixme'\nat xxx.t line 7."],
},
{
causes_failure => 0,
plan => 3,
trace_file => 'xxx.t',
trace_line => 8,
},
],
"Flattened events look like we expect"
);
See L<Test2::API::InterceptResult::Event> for a full description of what
C<flatten()> provides for each event.
=head1 METHODS
Please note that no methods modify the original instance unless asked to do so.
=head2 CONSTRUCTION
=over 4
=item $events = Test2::API::InterceptResult->new(@EVENTS)
=item $events = Test2::API::InterceptResult->new_from_ref(\@EVENTS)
These create a new instance of Test2::API::InterceptResult from the given
events.
In the first form a new blessed arrayref is returned. In the 'new_from_ref'
form the reference you pass in is directly blessed.
Both of these will throw an exception if called in void context. This is mainly
important for the 'filtering' methods listed below which normally return a new
instance, they throw an exception in such cases as it probably means someone
meant to filter the original in place.
=item $clone = $events->clone()
Make a clone of the original events. Note that this is a deep copy, the entire
structure is duplicated. This uses C<dclone> from L<Storable> to achieve the
deep clone.
=back
=head2 NORMALIZATION
=over 4
=item @events = $events->event_list
This returns all the events in list-form.
=item $hub = $events->hub
This returns a new L<Test2::Hub> instance that has processed all the events
contained in the instance. This gives you a simple way to inspect the state
changes your events cause.
=item $state = $events->state
This returns a summary of the state of a hub after processing all the events.
{
count => 2, # Number of assertions made
failed => 1, # Number of test failures seen
is_passing => 0, # Boolean, true if the test would be passing
# after the events are processed.
plan => 2, # Plan, either a number, undef, 'SKIP', or 'NO PLAN'
follows_plan => 1, # True if there is a plan and it was followed.
# False if the plan and assertions did not
# match, undef if no plan was present in the
# event list.
bailed_out => undef, # undef unless there was a bail-out in the
# events in which case this will be a string
# explaining why there was a bailout, if no
# reason was given this will simply be set to
# true (1).
skip_reason => undef, # If there was a skip_all this will give the
# reason.
}
=item $new = $events->upgrade
=item $events->upgrade(in_place => $BOOL)
B<Note:> This normally returns a new instance, leaving the original unchanged.
If you call it in void context it will throw an exception. If you want to
modify the original you must pass in the C<< in_place => 1 >> option. You may
call this in void context when you ask to modify it in place. The in-place form
returns the instance that was modified so you can chain methods.
This will create a clone of the list where all events have been converted into
L<Test2::API::InterceptResult::Event> instances. This is extremely helpful as
L<Test2::API::InterceptResult::Event> provide a much better interface for
working with events. This allows you to avoid thinking about legacy event
types.
This also means your tests against the list are not fragile if the tool
you are testing randomly changes what type of events it generates (IE Changing
from L<Test2::Event::Ok> to L<Test2::Event::Pass>, both make assertions and
both will normalize to identical (or close enough)
L<Test2::API::InterceptResult::Event> instances.
Really you almost always want this, the only reason it is not done
automatically is to make sure the C<intercept()> tool is backwards compatible.
=item $new = $events->squash_info
=item $events->squash_info(in_place => $BOOL)
B<Note:> This normally returns a new instance, leaving the original unchanged.
If you call it in void context it will throw an exception. If you want to
modify the original you must pass in the C<< in_place => 1 >> option. You may
call this in void context when you ask to modify it in place. The in-place form
returns the instance that was modified so you can chain methods.
B<Note:> All events in the new or modified instance will be converted to
L<Test2::API::InterceptResult::Event> instances. There is no way to avoid this,
the squash operation requires the upgraded event class.
L<Test::More> and many other legacy tools would send notes, diags, and
assertions as seperate events. A subtest in L<Test::More> would send a note
with the subtest name, the subtest assertion, and finally a diagnostics event
if the subtest failed. This method will normalize things by squashing the note
and diag into the same event as the subtest (This is different from putting
them into the subtest, which is not what happens).
=back
=head2 FILTERING
B<Note:> These normally return new instances, leaving the originals unchanged.
If you call them in void context they will throw exceptions. If you want to
modify the originals you must pass in the C<< in_place => 1 >> option. You may
call these in void context when you ask to modify them in place. The in-place
forms return the instance that was modified so you can chain methods.
=head3 %PARAMS
These all accept the same 2 optional parameters:
=over 4
=item in_place => $BOOL
When true the method will modify the instance in place instead of returning a
new instance.
=item args => \@ARGS
If you wish to pass parameters into the event method being used for filtering,
you may do so here.
=back
=head3 METHODS
=over 4
=item $events->grep($CALL, %PARAMS)
This is essentially:
Test2::API::InterceptResult->new(
grep { $_->$CALL( @{$PARAMS{args}} ) } $self->event_list,
);
B<Note:> that $CALL is called on an upgraded version of the event, though
the events returned will be the original ones, not the upgraded ones.
$CALL may be either the name of a method on
L<Test2::API::InterceptResult::Event>, or a coderef.
=item $events->asserts(%PARAMS)
This is essentially:
$events->grep(has_assert => @{$PARAMS{args}})
It returns a new instance containing only the events that made assertions.
=item $events->subtests(%PARAMS)
This is essentially:
$events->grep(has_subtest => @{$PARAMS{args}})
It returns a new instance containing only the events that have subtests.
=item $events->diags(%PARAMS)
This is essentially:
$events->grep(has_diags => @{$PARAMS{args}})
It returns a new instance containing only the events that have diags.
=item $events->notes(%PARAMS)
This is essentially:
$events->grep(has_notes => @{$PARAMS{args}})
It returns a new instance containing only the events that have notes.
=item $events->errors(%PARAMS)
B<Note:> Errors are NOT failing assertions. Failing assertions are a different
thing.
This is essentially:
$events->grep(has_errors => @{$PARAMS{args}})
It returns a new instance containing only the events that have errors.
=item $events->plans(%PARAMS)
This is essentially:
$events->grep(has_plan => @{$PARAMS{args}})
It returns a new instance containing only the events that set the plan.
=item $events->causes_fail(%PARAMS)
=item $events->causes_failure(%PARAMS)
These are essentially:
$events->grep(causes_fail => @{$PARAMS{args}})
$events->grep(causes_failure => @{$PARAMS{args}})
B<Note:> C<causes_fail()> and C<causes_failure()> are both aliases for
eachother in events, so these methods are effectively aliases here as well.
It returns a new instance containing only the events that cause failure.
=back
=head2 MAPPING
These methods B<ALWAYS> return an arrayref.
B<Note:> No methods on L<Test2::API::InterceptResult::Event> alter the event in
any way.
B<Important Notes about Events>:
L<Test2::API::InterceptResult::Event> was tailor-made to be used in
event-lists. Most methods that are not applicable to a given event will return
an empty list, so you normally do not need to worry about unwanted C<undef>
values or exceptions being thrown. Mapping over event methods is an entended
use, so it works well to produce lists.
B<Exceptions to the rule:>
Some methods such as C<causes_fail> always return a boolean true or false for
all events. Any method prefixed with C<the_> conveys the intent that the event
should have exactly 1 of something, so those will throw an exception when that
condition is not true.
=over 4
=item $arrayref = $events->map($CALL, %PARAMS)
This is essentially:
[ map { $_->$CALL(@{ $PARAMS{args} }) } $events->upgrade->event_list ];
$CALL may be either the name of a method on
L<Test2::API::InterceptResult::Event>, or a coderef.
=item $arrayref = $events->flatten(%PARAMS)
This is essentially:
[ map { $_->flatten(@{ $PARAMS{args} }) } $events->upgrade->event_list ];
It returns a new list of flattened structures.
See L<Test2::API::InterceptResult::Event> for details on what C<flatten()>
returns.
=item $arrayref = $events->briefs(%PARAMS)
This is essentially:
[ map { $_->briefs(@{ $PARAMS{args} }) } $events->upgrade->event_list ];
It returns a new list of event briefs.
See L<Test2::API::InterceptResult::Event> for details on what C<brief()>
returns.
=item $arrayref = $events->summaries(%PARAMS)
This is essentially:
[ map { $_->summaries(@{ $PARAMS{args} }) } $events->upgrade->event_list ];
It returns a new list of event summaries.
See L<Test2::API::InterceptResult::Event> for details on what C<summary()>
returns.
=item $arrayref = $events->subtest_results(%PARAMS)
This is essentially:
[ map { $_->subtest_result(@{ $PARAMS{args} }) } $events->upgrade->event_list ];
It returns a new list of event summaries.
See L<Test2::API::InterceptResult::Event> for details on what
C<subtest_result()> returns.
=item $arrayref = $events->diag_messages(%PARAMS)
This is essentially:
[ map { $_->diag_messages(@{ $PARAMS{args} }) } $events->upgrade->event_list ];
It returns a new list of diagnostic messages (strings).
See L<Test2::API::InterceptResult::Event> for details on what
C<diag_messages()> returns.
=item $arrayref = $events->note_messages(%PARAMS)
This is essentially:
[ map { $_->note_messages(@{ $PARAMS{args} }) } $events->upgrade->event_list ];
It returns a new list of notification messages (strings).
See L<Test2::API::InterceptResult::Event> for details on what
C<note_messages()> returns.
=item $arrayref = $events->error_messages(%PARAMS)
This is essentially:
[ map { $_->error_messages(@{ $PARAMS{args} }) } $events->upgrade->event_list ];
It returns a new list of error messages (strings).
See L<Test2::API::InterceptResult::Event> for details on what
C<error_messages()> returns.
=back
=head1 SOURCE
The source code repository for Test2 can be found at
F<http://github.com/Test-More/test-more/>.
=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 2020 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,25 @@
package Test2::API::InterceptResult::Facet;
use strict;
use warnings;
our $VERSION = '1.302183';
BEGIN {
require Test2::EventFacet;
our @ISA = ('Test2::EventFacet');
}
our $AUTOLOAD;
sub AUTOLOAD {
my $self = shift;
my $name = $AUTOLOAD;
$name =~ s/^.*:://g;
return undef unless exists $self->{$name};
return $self->{$name};
}
sub DESTROY {}
1;

View File

@@ -0,0 +1,66 @@
package Test2::API::InterceptResult::Hub;
use strict;
use warnings;
our $VERSION = '1.302183';
BEGIN { require Test2::Hub; our @ISA = qw(Test2::Hub) }
use Test2::Util::HashBase;
sub init {
my $self = shift;
$self->SUPER::init();
$self->{+NESTED} = 0;
}
sub inherit {
my $self = shift;
$self->{+NESTED} = 0;
}
sub terminate { }
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::API::InterceptResult::Hub - Hub used by InterceptResult.
=head1 SOURCE
The source code repository for Test2 can be found at
F<http://github.com/Test-More/test-more/>.
=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 2020 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,196 @@
package Test2::API::InterceptResult::Squasher;
use strict;
use warnings;
our $VERSION = '1.302183';
use Carp qw/croak/;
use List::Util qw/first/;
use Test2::Util::HashBase qw{
<events
+down_sig +down_buffer
+up_into +up_sig +up_clear
};
sub init {
my $self = shift;
croak "'events' is a required attribute" unless $self->{+EVENTS};
}
sub can_squash {
my $self = shift;
my ($event) = @_;
# No info, no squash
return unless $event->has_info;
# Do not merge up if one of these is true
return if first { $event->$_ } 'causes_fail', 'has_assert', 'has_bailout', 'has_errors', 'has_plan', 'has_subtest';
# Signature if we can squash
return $event->trace_signature;
}
sub process {
my $self = shift;
my ($event) = @_;
return if $self->squash_up($event);
return if $self->squash_down($event);
$self->flush_down($event);
push @{$self->{+EVENTS}} => $event;
return;
}
sub squash_down {
my $self = shift;
my ($event) = @_;
my $sig = $self->can_squash($event)
or return;
$self->flush_down()
if $self->{+DOWN_SIG} && $self->{+DOWN_SIG} ne $sig;
$self->{+DOWN_SIG} ||= $sig;
push @{$self->{+DOWN_BUFFER}} => $event;
return 1;
}
sub flush_down {
my $self = shift;
my ($into) = @_;
my $sig = delete $self->{+DOWN_SIG};
my $buffer = delete $self->{+DOWN_BUFFER};
return unless $buffer && @$buffer;
my $fsig = $into ? $into->trace_signature : undef;
if ($fsig && $fsig eq $sig) {
$self->squash($into, @$buffer);
}
else {
push @{$self->{+EVENTS}} => @$buffer if $buffer;
}
}
sub clear_up {
my $self = shift;
return unless $self->{+UP_CLEAR};
delete $self->{+UP_INTO};
delete $self->{+UP_SIG};
delete $self->{+UP_CLEAR};
}
sub squash_up {
my $self = shift;
my ($event) = @_;
no warnings 'uninitialized';
$self->clear_up;
if ($event->has_assert) {
if(my $sig = $event->trace_signature) {
$self->{+UP_INTO} = $event;
$self->{+UP_SIG} = $sig;
$self->{+UP_CLEAR} = 0;
}
else {
$self->{+UP_CLEAR} = 1;
$self->clear_up;
}
return;
}
my $into = $self->{+UP_INTO} or return;
# Next iteration should clear unless something below changes that
$self->{+UP_CLEAR} = 1;
# Only merge into matching trace signatres
my $sig = $self->can_squash($event);
return unless $sig eq $self->{+UP_SIG};
# OK Merge! Do not clear merge in case the return event is also a matching sig diag-only
$self->{+UP_CLEAR} = 0;
$self->squash($into, $event);
return 1;
}
sub squash {
my $self = shift;
my ($into, @from) = @_;
push @{$into->facet_data->{info}} => $_->info for @from;
}
sub DESTROY {
my $self = shift;
return unless $self->{+EVENTS};
$self->flush_down();
return;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::API::InterceptResult::Squasher - Encapsulation of the algorithm that
squashes diags into assertions.
=head1 DESCRIPTION
Internal use only, please ignore.
=head1 SOURCE
The source code repository for Test2 can be found at
F<http://github.com/Test-More/test-more/>.
=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 2020 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,226 @@
package Test2::API::Stack;
use strict;
use warnings;
our $VERSION = '1.302183';
use Test2::Hub();
use Carp qw/confess/;
sub new {
my $class = shift;
return bless [], $class;
}
sub new_hub {
my $self = shift;
my %params = @_;
my $class = delete $params{class} || 'Test2::Hub';
my $hub = $class->new(%params);
if (@$self) {
$hub->inherit($self->[-1], %params);
}
else {
require Test2::API;
$hub->format(Test2::API::test2_formatter()->new_root)
unless $hub->format || exists($params{formatter});
my $ipc = Test2::API::test2_ipc();
if ($ipc && !$hub->ipc && !exists($params{ipc})) {
$hub->set_ipc($ipc);
$ipc->add_hub($hub->hid);
}
}
push @$self => $hub;
$hub;
}
sub top {
my $self = shift;
return $self->new_hub unless @$self;
return $self->[-1];
}
sub peek {
my $self = shift;
return @$self ? $self->[-1] : undef;
}
sub cull {
my $self = shift;
$_->cull for reverse @$self;
}
sub all {
my $self = shift;
return @$self;
}
sub root {
my $self = shift;
return unless @$self;
return $self->[0];
}
sub clear {
my $self = shift;
@$self = ();
}
# Do these last without keywords in order to prevent them from getting used
# when we want the real push/pop.
{
no warnings 'once';
*push = sub {
my $self = shift;
my ($hub) = @_;
$hub->inherit($self->[-1]) if @$self;
push @$self => $hub;
};
*pop = sub {
my $self = shift;
my ($hub) = @_;
confess "No hubs on the stack"
unless @$self;
confess "You cannot pop the root hub"
if 1 == @$self;
confess "Hub stack mismatch, attempted to pop incorrect hub"
unless $self->[-1] == $hub;
pop @$self;
};
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::API::Stack - Object to manage a stack of L<Test2::Hub>
instances.
=head1 ***INTERNALS NOTE***
B<The internals of this package are subject to change at any time!> The public
methods provided will not change in backwards incompatible ways, but the
underlying implementation details might. B<Do not break encapsulation here!>
=head1 DESCRIPTION
This module is used to represent and manage a stack of L<Test2::Hub>
objects. Hubs are usually in a stack so that you can push a new hub into place
that can intercept and handle events differently than the primary hub.
=head1 SYNOPSIS
my $stack = Test2::API::Stack->new;
my $hub = $stack->top;
=head1 METHODS
=over 4
=item $stack = Test2::API::Stack->new()
This will create a new empty stack instance. All arguments are ignored.
=item $hub = $stack->new_hub()
=item $hub = $stack->new_hub(%params)
=item $hub = $stack->new_hub(%params, class => $class)
This will generate a new hub and push it to the top of the stack. Optionally
you can provide arguments that will be passed into the constructor for the
L<Test2::Hub> object.
If you specify the C<< 'class' => $class >> argument, the new hub will be an
instance of the specified class.
Unless your parameters specify C<'formatter'> or C<'ipc'> arguments, the
formatter and IPC instance will be inherited from the current top hub. You can
set the parameters to C<undef> to avoid having a formatter or IPC instance.
If there is no top hub, and you do not ask to leave IPC and formatter undef,
then a new formatter will be created, and the IPC instance from
L<Test2::API> will be used.
=item $hub = $stack->top()
This will return the top hub from the stack. If there is no top hub yet this
will create it.
=item $hub = $stack->peek()
This will return the top hub from the stack. If there is no top hub yet this
will return undef.
=item $stack->cull
This will call C<< $hub->cull >> on all hubs in the stack.
=item @hubs = $stack->all
This will return all the hubs in the stack as a list.
=item $stack->clear
This will completely remove all hubs from the stack. Normally you do not want
to do this, but there are a few valid reasons for it.
=item $stack->push($hub)
This will push the new hub onto the stack.
=item $stack->pop($hub)
This will pop a hub from the stack, if the hub at the top of the stack does not
match the hub you expect (passed in as an argument) it will throw an exception.
=back
=head1 SOURCE
The source code repository for Test2 can be found at
F<http://github.com/Test-More/test-more/>.
=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 2020 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