Spaces:
Sleeping
Sleeping
package Test2::Event; | |
use strict; | |
use warnings; | |
our $VERSION = '1.302183'; | |
use Scalar::Util qw/blessed reftype/; | |
use Carp qw/croak/; | |
use Test2::Util::HashBase qw/trace -amnesty uuid -_eid -hubs/; | |
use Test2::Util::ExternalMeta qw/meta get_meta set_meta delete_meta/; | |
use Test2::Util qw/pkg_to_file gen_uid/; | |
use Test2::EventFacet::About(); | |
use Test2::EventFacet::Amnesty(); | |
use Test2::EventFacet::Assert(); | |
use Test2::EventFacet::Control(); | |
use Test2::EventFacet::Error(); | |
use Test2::EventFacet::Info(); | |
use Test2::EventFacet::Meta(); | |
use Test2::EventFacet::Parent(); | |
use Test2::EventFacet::Plan(); | |
use Test2::EventFacet::Trace(); | |
use Test2::EventFacet::Hub(); | |
# Legacy tools will expect this to be loaded now | |
require Test2::Util::Trace; | |
my %LOADED_FACETS = ( | |
'about' => 'Test2::EventFacet::About', | |
'amnesty' => 'Test2::EventFacet::Amnesty', | |
'assert' => 'Test2::EventFacet::Assert', | |
'control' => 'Test2::EventFacet::Control', | |
'errors' => 'Test2::EventFacet::Error', | |
'info' => 'Test2::EventFacet::Info', | |
'meta' => 'Test2::EventFacet::Meta', | |
'parent' => 'Test2::EventFacet::Parent', | |
'plan' => 'Test2::EventFacet::Plan', | |
'trace' => 'Test2::EventFacet::Trace', | |
'hubs' => 'Test2::EventFacet::Hub', | |
); | |
sub FACET_TYPES { sort values %LOADED_FACETS } | |
sub load_facet { | |
my $class = shift; | |
my ($facet) = @_; | |
return $LOADED_FACETS{$facet} if exists $LOADED_FACETS{$facet}; | |
my @check = ($facet); | |
if ('s' eq substr($facet, -1, 1)) { | |
push @check => substr($facet, 0, -1); | |
} | |
else { | |
push @check => $facet . 's'; | |
} | |
my $found; | |
for my $check (@check) { | |
my $mod = "Test2::EventFacet::" . ucfirst($facet); | |
my $file = pkg_to_file($mod); | |
next unless eval { require $file; 1 }; | |
$found = $mod; | |
last; | |
} | |
return undef unless $found; | |
$LOADED_FACETS{$facet} = $found; | |
} | |
sub causes_fail { 0 } | |
sub increments_count { 0 } | |
sub diagnostics { 0 } | |
sub no_display { 0 } | |
sub subtest_id { undef } | |
sub callback { } | |
sub terminate { () } | |
sub global { () } | |
sub sets_plan { () } | |
sub summary { ref($_[0]) } | |
sub related { | |
my $self = shift; | |
my ($event) = @_; | |
my $tracea = $self->trace or return undef; | |
my $traceb = $event->trace or return undef; | |
my $uuida = $tracea->uuid; | |
my $uuidb = $traceb->uuid; | |
if ($uuida && $uuidb) { | |
return 1 if $uuida eq $uuidb; | |
return 0; | |
} | |
my $siga = $tracea->signature or return undef; | |
my $sigb = $traceb->signature or return undef; | |
return 1 if $siga eq $sigb; | |
return 0; | |
} | |
sub add_hub { | |
my $self = shift; | |
unshift @{$self->{+HUBS}} => @_; | |
} | |
sub add_amnesty { | |
my $self = shift; | |
for my $am (@_) { | |
$am = {%$am} if ref($am) ne 'ARRAY'; | |
$am = Test2::EventFacet::Amnesty->new($am); | |
push @{$self->{+AMNESTY}} => $am; | |
} | |
} | |
sub eid { $_[0]->{+_EID} ||= gen_uid() } | |
sub common_facet_data { | |
my $self = shift; | |
my %out; | |
$out{about} = {package => ref($self) || undef}; | |
if (my $uuid = $self->uuid) { | |
$out{about}->{uuid} = $uuid; | |
} | |
$out{about}->{eid} = $self->{+_EID} || $self->eid; | |
if (my $trace = $self->trace) { | |
$out{trace} = { %$trace }; | |
} | |
if (my $hubs = $self->hubs) { | |
$out{hubs} = $hubs; | |
} | |
$out{amnesty} = [map {{ %{$_} }} @{$self->{+AMNESTY}}] | |
if $self->{+AMNESTY}; | |
if (my $meta = $self->meta_facet_data) { | |
$out{meta} = $meta; | |
} | |
return \%out; | |
} | |
sub meta_facet_data { | |
my $self = shift; | |
my $key = Test2::Util::ExternalMeta::META_KEY(); | |
my $hash = $self->{$key} or return undef; | |
return {%$hash}; | |
} | |
sub facet_data { | |
my $self = shift; | |
my $out = $self->common_facet_data; | |
$out->{about}->{details} = $self->summary || undef; | |
$out->{about}->{no_display} = $self->no_display || undef; | |
# Might be undef, we want to preserve that | |
my $terminate = $self->terminate; | |
$out->{control} = { | |
global => $self->global || 0, | |
terminate => $terminate, | |
has_callback => $self->can('callback') == \&callback ? 0 : 1, | |
}; | |
$out->{assert} = { | |
no_debug => 1, # Legacy behavior | |
pass => $self->causes_fail ? 0 : 1, | |
details => $self->summary, | |
} if $self->increments_count; | |
$out->{parent} = {hid => $self->subtest_id} if $self->subtest_id; | |
if (my @plan = $self->sets_plan) { | |
$out->{plan} = {}; | |
$out->{plan}->{count} = $plan[0] if defined $plan[0]; | |
$out->{plan}->{details} = $plan[2] if defined $plan[2]; | |
if ($plan[1]) { | |
$out->{plan}->{skip} = 1 if $plan[1] eq 'SKIP'; | |
$out->{plan}->{none} = 1 if $plan[1] eq 'NO PLAN'; | |
} | |
$out->{control}->{terminate} ||= 0 if $out->{plan}->{skip}; | |
} | |
if ($self->causes_fail && !$out->{assert}) { | |
$out->{errors} = [ | |
{ | |
tag => 'FAIL', | |
fail => 1, | |
details => $self->summary, | |
} | |
]; | |
} | |
my %IGNORE = (trace => 1, about => 1, control => 1); | |
my $do_info = !grep { !$IGNORE{$_} } keys %$out; | |
if ($do_info && !$self->no_display && $self->diagnostics) { | |
$out->{info} = [ | |
{ | |
tag => 'DIAG', | |
debug => 1, | |
details => $self->summary, | |
} | |
]; | |
} | |
return $out; | |
} | |
sub facets { | |
my $self = shift; | |
my %out; | |
my $data = $self->facet_data; | |
my @errors = $self->validate_facet_data($data); | |
die join "\n" => @errors if @errors; | |
for my $facet (keys %$data) { | |
my $class = $self->load_facet($facet); | |
my $val = $data->{$facet}; | |
unless($class) { | |
$out{$facet} = $val; | |
next; | |
} | |
my $is_list = reftype($val) eq 'ARRAY' ? 1 : 0; | |
if ($is_list) { | |
$out{$facet} = [map { $class->new($_) } @$val]; | |
} | |
else { | |
$out{$facet} = $class->new($val); | |
} | |
} | |
return \%out; | |
} | |
sub validate_facet_data { | |
my $class_or_self = shift; | |
my ($f, %params); | |
$f = shift if @_ && (reftype($_[0]) || '') eq 'HASH'; | |
%params = @_; | |
$f ||= $class_or_self->facet_data if blessed($class_or_self); | |
croak "No facet data" unless $f; | |
my @errors; | |
for my $k (sort keys %$f) { | |
my $fclass = $class_or_self->load_facet($k); | |
push @errors => "Could not find a facet class for facet '$k'" | |
if $params{require_facet_class} && !$fclass; | |
next unless $fclass; | |
my $v = $f->{$k}; | |
next unless defined($v); # undef is always fine | |
my $is_list = $fclass->is_list(); | |
my $got_list = reftype($v) eq 'ARRAY' ? 1 : 0; | |
push @errors => "Facet '$k' should be a list, but got a single item ($v)" | |
if $is_list && !$got_list; | |
push @errors => "Facet '$k' should not be a list, but got a a list ($v)" | |
if $got_list && !$is_list; | |
} | |
return @errors; | |
} | |
sub nested { | |
my $self = shift; | |
Carp::cluck("Use of Test2::Event->nested() is deprecated, use Test2::Event->trace->nested instead") | |
if $ENV{AUTHOR_TESTING}; | |
if (my $hubs = $self->{+HUBS}) { | |
return $hubs->[0]->{nested} if @$hubs; | |
} | |
my $trace = $self->{+TRACE} or return undef; | |
return $trace->{nested}; | |
} | |
sub in_subtest { | |
my $self = shift; | |
Carp::cluck("Use of Test2::Event->in_subtest() is deprecated, use Test2::Event->trace->hid instead") | |
if $ENV{AUTHOR_TESTING}; | |
my $hubs = $self->{+HUBS}; | |
if ($hubs && @$hubs) { | |
return undef unless $hubs->[0]->{nested}; | |
return $hubs->[0]->{hid} | |
} | |
my $trace = $self->{+TRACE} or return undef; | |
return undef unless $trace->{nested}; | |
return $trace->{hid}; | |
} | |
1; | |
__END__ | |
=pod | |
=encoding UTF-8 | |
=head1 NAME | |
Test2::Event - Base class for events | |
=head1 DESCRIPTION | |
Base class for all event objects that get passed through | |
L<Test2>. | |
=head1 SYNOPSIS | |
package Test2::Event::MyEvent; | |
use strict; | |
use warnings; | |
# This will make our class an event subclass (required) | |
use base 'Test2::Event'; | |
# Add some accessors (optional) | |
# You are not obligated to use HashBase, you can use any object tool you | |
# want, or roll your own accessors. | |
use Test2::Util::HashBase qw/foo bar baz/; | |
# Use this if you want the legacy API to be written for you, for this to | |
# work you will need to implement a facet_data() method. | |
use Test2::Util::Facets2Legacy; | |
# Chance to initialize some defaults | |
sub init { | |
my $self = shift; | |
# no other args in @_ | |
$self->set_foo('xxx') unless defined $self->foo; | |
... | |
} | |
# This is the new way for events to convey data to the Test2 system | |
sub facet_data { | |
my $self = shift; | |
# Get common facets such as 'about', 'trace' 'amnesty', and 'meta' | |
my $facet_data = $self->common_facet_data(); | |
# Are you making an assertion? | |
$facet_data->{assert} = {pass => 1, details => 'my assertion'}; | |
... | |
return $facet_data; | |
} | |
1; | |
=head1 METHODS | |
=head2 GENERAL | |
=over 4 | |
=item $trace = $e->trace | |
Get a snapshot of the L<Test2::EventFacet::Trace> as it was when this event was | |
generated | |
=item $bool_or_undef = $e->related($e2) | |
Check if 2 events are related. In this case related means their traces share a | |
signature meaning they were created with the same context (or at the very least | |
by contexts which share an id, which is the same thing unless someone is doing | |
something very bad). | |
This can be used to reliably link multiple events created by the same tool. For | |
instance a failing test like C<ok(0, "fail"> will generate 2 events, one being | |
a L<Test2::Event::Ok>, the other being a L<Test2::Event::Diag>, both of these | |
events are related having been created under the same context and by the same | |
initial tool (though multiple tools may have been nested under the initial | |
one). | |
This will return C<undef> if the relationship cannot be checked, which happens | |
if either event has an incomplete or missing trace. This will return C<0> if | |
the traces are complete, but do not match. C<1> will be returned if there is a | |
match. | |
=item $e->add_amnesty({tag => $TAG, details => $DETAILS}); | |
This can be used to add amnesty to this event. Amnesty only effects failing | |
assertions in most cases, but some formatters may display them for passing | |
assertions, or even non-assertions as well. | |
Amnesty will prevent a failed assertion from causing the overall test to fail. | |
In other words it marks a failure as expected and allowed. | |
B<Note:> This is how 'TODO' is implemented under the hood. TODO is essentially | |
amnesty with the 'TODO' tag. The details are the reason for the TODO. | |
=item $uuid = $e->uuid | |
If UUID tagging is enabled (See L<Test::API>) then any event that has made its | |
way through a hub will be tagged with a UUID. A newly created event will not | |
yet be tagged in most cases. | |
=item $class = $e->load_facet($name) | |
This method is used to load a facet by name (or key). It will attempt to load | |
the facet class, if it succeeds it will return the class it loaded. If it fails | |
it will return C<undef>. This caches the result at the class level so that | |
future calls will be faster. | |
The C<$name> variable should be the key used to access the facet in a facets | |
hashref. For instance the assertion facet has the key 'assert', the information | |
facet has the 'info' key, and the error facet has the key 'errors'. You may | |
include or omit the 's' at the end of the name, the method is smart enough to | |
try both the 's' and no-'s' forms, it will check what you provided first, and | |
if that is not found it will add or strip the 's and try again. | |
=item @classes = $e->FACET_TYPES() | |
=item @classes = Test2::Event->FACET_TYPES() | |
This returns a list of all facets that have been loaded using the | |
C<load_facet()> method. This will not return any classes that have not been | |
loaded, or have been loaded directly without a call to C<load_facet()>. | |
B<Note:> The core facet types are automatically loaded and populated in this | |
list. | |
=back | |
=head2 NEW API | |
=over 4 | |
=item $hashref = $e->common_facet_data(); | |
This can be used by subclasses to generate a starting facet data hashref. This | |
will populate the hashref with the trace, meta, amnesty, and about facets. | |
These facets are nearly always produced the same way for all events. | |
=item $hashref = $e->facet_data() | |
If you do not override this then the default implementation will attempt to | |
generate facets from the legacy API. This generation is limited only to what | |
the legacy API can provide. It is recommended that you override this method and | |
write out explicit facet data. | |
=item $hashref = $e->facets() | |
This takes the hashref from C<facet_data()> and blesses each facet into the | |
proper C<Test2::EventFacet::*> subclass. If no class can be found for any given | |
facet it will be passed along unchanged. | |
=item @errors = $e->validate_facet_data(); | |
=item @errors = $e->validate_facet_data(%params); | |
=item @errors = $e->validate_facet_data(\%facets, %params); | |
=item @errors = Test2::Event->validate_facet_data(%params); | |
=item @errors = Test2::Event->validate_facet_data(\%facets, %params); | |
This method will validate facet data and return a list of errors. If no errors | |
are found this will return an empty list. | |
This can be called as an object method with no arguments, in which case the | |
C<facet_data()> method will be called to get the facet data to be validated. | |
When used as an object method the C<\%facet_data> argument may be omitted. | |
When used as a class method the C<\%facet_data> argument is required. | |
Remaining arguments will be slurped into a C<%params> hash. | |
Currently only 1 parameter is defined: | |
=over 4 | |
=item require_facet_class => $BOOL | |
When set to true (default is false) this will reject any facets where a facet | |
class cannot be found. Normally facets without classes are assumed to be custom | |
and are ignored. | |
=back | |
=back | |
=head3 WHAT ARE FACETS? | |
Facets are how events convey their purpose to the Test2 internals and | |
formatters. An event without facets will have no intentional effect on the | |
overall test state, and will not be displayed at all by most formatters, except | |
perhaps to say that an event of an unknown type was seen. | |
Facets are produced by the C<facet_data()> subroutine, which you should | |
nearly-always override. C<facet_data()> is expected to return a hashref where | |
each key is the facet type, and the value is either a hashref with the data for | |
that facet, or an array of hashrefs. Some facets must be defined as single | |
hashrefs, some must be defined as an array of hashrefs, No facets allow both. | |
C<facet_data()> B<MUST NOT> bless the data it returns, the main hashref, and | |
nested facet hashrefs B<MUST> be bare, though items contained within each | |
facet may be blessed. The data returned by this method B<should> also be copies | |
of the internal data in order to prevent accidental state modification. | |
C<facets()> takes the data from C<facet_data()> and blesses it into the | |
C<Test2::EventFacet::*> packages. This is rarely used however, the EventFacet | |
packages are primarily for convenience and documentation. The EventFacet | |
classes are not used at all internally, instead the raw data is used. | |
Here is a list of facet types by package. The packages are not used internally, | |
but are where the documentation for each type is kept. | |
B<Note:> Every single facet type has the C<'details'> field. This field is | |
always intended for human consumption, and when provided, should explain the | |
'why' for the facet. All other fields are facet specific. | |
=over 4 | |
=item about => {...} | |
L<Test2::EventFacet::About> | |
This contains information about the event itself such as the event package | |
name. The C<details> field for this facet is an overall summary of the event. | |
=item assert => {...} | |
L<Test2::EventFacet::Assert> | |
This facet is used if an assertion was made. The C<details> field of this facet | |
is the description of the assertion. | |
=item control => {...} | |
L<Test2::EventFacet::Control> | |
This facet is used to tell the L<Test2::Event::Hub> about special actions the | |
event causes. Things like halting all testing, terminating the current test, | |
etc. In this facet the C<details> field explains why any special action was | |
taken. | |
B<Note:> This is how bail-out is implemented. | |
=item meta => {...} | |
L<Test2::EventFacet::Meta> | |
The meta facet contains all the meta-data attached to the event. In this case | |
the C<details> field has no special meaning, but may be present if something | |
sets the 'details' meta-key on the event. | |
=item parent => {...} | |
L<Test2::EventFacet::Parent> | |
This facet contains nested events and similar details for subtests. In this | |
facet the C<details> field will typically be the name of the subtest. | |
=item plan => {...} | |
L<Test2::EventFacet::Plan> | |
This facet tells the system that a plan has been set. The C<details> field of | |
this is usually left empty, but when present explains why the plan is what it | |
is, this is most useful if the plan is to skip-all. | |
=item trace => {...} | |
L<Test2::EventFacet::Trace> | |
This facet contains information related to when and where the event was | |
generated. This is how the test file and line number of a failure is known. | |
This facet can also help you to tell if tests are related. | |
In this facet the C<details> field overrides the "failed at test_file.t line | |
42." message provided on assertion failure. | |
=item amnesty => [{...}, ...] | |
L<Test2::EventFacet::Amnesty> | |
The amnesty facet is a list instead of a single item, this is important as | |
amnesty can come from multiple places at once. | |
For each instance of amnesty the C<details> field explains why amnesty was | |
granted. | |
B<Note:> Outside of formatters amnesty only acts to forgive a failing | |
assertion. | |
=item errors => [{...}, ...] | |
L<Test2::EventFacet::Error> | |
The errors facet is a list instead of a single item, any number of errors can | |
be listed. In this facet C<details> describes the error, or may contain the raw | |
error message itself (such as an exception). In perl exception may be blessed | |
objects, as such the raw data for this facet may contain nested items which are | |
blessed. | |
Not all errors are considered fatal, there is a C<fail> field that must be set | |
for an error to cause the test to fail. | |
B<Note:> This facet is unique in that the field name is 'errors' while the | |
package is 'Error'. This is because this is the only facet type that is both a | |
list, and has a name where the plural is not the same as the singular. This may | |
cause some confusion, but I feel it will be less confusing than the | |
alternative. | |
=item info => [{...}, ...] | |
L<Test2::EventFacet::Info> | |
The 'info' facet is a list instead of a single item, any quantity of extra | |
information can be attached to an event. Some information may be critical | |
diagnostics, others may be simply commentary in nature, this is determined by | |
the C<debug> flag. | |
For this facet the C<details> flag is the info itself. This info may be a | |
string, or it may be a data structure to display. This is one of the few facet | |
types that may contain blessed items. | |
=back | |
=head2 LEGACY API | |
=over 4 | |
=item $bool = $e->causes_fail | |
Returns true if this event should result in a test failure. In general this | |
should be false. | |
=item $bool = $e->increments_count | |
Should be true if this event should result in a test count increment. | |
=item $e->callback($hub) | |
If your event needs to have extra effects on the L<Test2::Hub> you can override | |
this method. | |
This is called B<BEFORE> your event is passed to the formatter. | |
=item $num = $e->nested | |
If this event is nested inside of other events, this should be the depth of | |
nesting. (This is mainly for subtests) | |
=item $bool = $e->global | |
Set this to true if your event is global, that is ALL threads and processes | |
should see it no matter when or where it is generated. This is not a common | |
thing to want, it is used by bail-out and skip_all to end testing. | |
=item $code = $e->terminate | |
This is called B<AFTER> your event has been passed to the formatter. This | |
should normally return undef, only change this if your event should cause the | |
test to exit immediately. | |
If you want this event to cause the test to exit you should return the exit | |
code here. Exit code of 0 means exit success, any other integer means exit with | |
failure. | |
This is used by L<Test2::Event::Plan> to exit 0 when the plan is | |
'skip_all'. This is also used by L<Test2::Event:Bail> to force the test | |
to exit with a failure. | |
This is called after the event has been sent to the formatter in order to | |
ensure the event is seen and understood. | |
=item $msg = $e->summary | |
This is intended to be a human readable summary of the event. This should | |
ideally only be one line long, but you can use multiple lines if necessary. This | |
is intended for human consumption. You do not need to make it easy for machines | |
to understand. | |
The default is to simply return the event package name. | |
=item ($count, $directive, $reason) = $e->sets_plan() | |
Check if this event sets the testing plan. It will return an empty list if it | |
does not. If it does set the plan it will return a list of 1 to 3 items in | |
order: Expected Test Count, Test Directive, Reason for directive. | |
=item $bool = $e->diagnostics | |
True if the event contains diagnostics info. This is useful because a | |
non-verbose harness may choose to hide events that are not in this category. | |
Some formatters may choose to send these to STDERR instead of STDOUT to ensure | |
they are seen. | |
=item $bool = $e->no_display | |
False by default. This will return true on events that should not be displayed | |
by formatters. | |
=item $id = $e->in_subtest | |
If the event is inside a subtest this should have the subtest ID. | |
=item $id = $e->subtest_id | |
If the event is a final subtest event, this should contain the subtest ID. | |
=back | |
=head1 THIRD PARTY META-DATA | |
This object consumes L<Test2::Util::ExternalMeta> which provides a consistent | |
way for you to attach meta-data to instances of this class. This is useful for | |
tools, plugins, and other extensions. | |
=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>[email protected]<gt> | |
=back | |
=head1 AUTHORS | |
=over 4 | |
=item Chad Granum E<lt>[email protected]<gt> | |
=back | |
=head1 COPYRIGHT | |
Copyright 2020 Chad Granum E<lt>[email protected]<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 | |