package Test2::Workflow; use strict; use warnings; our $VERSION = '0.000156'; our @EXPORT_OK = qw/parse_args current_build build root_build init_root build_stack/; use base 'Exporter'; use Test2::Workflow::Build; use Test2::Workflow::Task::Group; use Test2::API qw/intercept/; use Scalar::Util qw/blessed/; sub parse_args { my %input = @_; my $args = delete $input{args}; my %out; my %props; my $caller = $out{frame} = $input{caller} || caller(defined $input{level} ? $input{level} : 1); delete @input{qw/caller level/}; for my $arg (@$args) { if (my $r = ref($arg)) { if ($r eq 'HASH') { %props = (%props, %$arg); } elsif ($r eq 'CODE') { die "Code is already set, did you provide multiple code blocks at $caller->[1] line $caller->[2].\n" if $out{code}; $out{code} = $arg } else { die "Not sure what to do with $arg at $caller->[1] line $caller->[2].\n"; } next; } if ($arg =~ m/^\d+$/) { push @{$out{lines}} => $arg; next; } die "Name is already set to '$out{name}', cannot set to '$arg', did you specify multiple names at $caller->[1] line $caller->[2].\n" if $out{name}; $out{name} = $arg; } die "a name must be provided, and must be truthy at $caller->[1] line $caller->[2].\n" unless $out{name}; die "a codeblock must be provided at $caller->[1] line $caller->[2].\n" unless $out{code}; return { %props, %out, %input }; } { my %ROOT_BUILDS; my @BUILD_STACK; sub root_build { $ROOT_BUILDS{$_[0]} } sub current_build { @BUILD_STACK ? $BUILD_STACK[-1] : undef } sub build_stack { @BUILD_STACK } sub init_root { my ($pkg, %args) = @_; $ROOT_BUILDS{$pkg} ||= Test2::Workflow::Build->new( name => $pkg, flat => 1, iso => 0, async => 0, is_root => 1, %args, ); return $ROOT_BUILDS{$pkg}; } sub build { my %params = @_; my $args = parse_args(%params); my $build = Test2::Workflow::Build->new(%$args); return $build if $args->{skip}; push @BUILD_STACK => $build; my ($ok, $err); my $events = intercept { my $todo = $args->{todo} ? Test2::Todo->new(reason => $args->{todo}) : undef; $ok = eval { $args->{code}->(); 1 }; $err = $@; $todo->end if $todo; }; # Clear the stash $build->{stash} = []; $build->set_events($events); pop @BUILD_STACK; unless($ok) { my $hub = Test2::API::test2_stack->top; my $count = @$events; my $list = $count ? "Overview of unseen events:\n" . join "" => map " " . blessed($_) . " " . $_->trace($hub)->debug . "\n", @$events : ""; die <<" EOT"; Exception in build '$args->{name}' with $count unseen event(s). $err $list EOT } return $build; } } 1; __END__ =pod =encoding UTF-8 =head1 NAME Test2::Workflow - A test workflow is a way of structuring tests using composable units. =head1 DESCRIPTION A test workflow is a way of structuring tests using composable units. A well known example of a test workflow is L. RSPEC is implemented using Test2::Workflow in L along with several extensions. =head1 IMPORTANT CONCEPTS =head2 BUILD L A Build is used to compose tasks. Usually a build object is pushed to the stack before running code that adds tasks to the build. Once the build sub is complete the build is popped and returned. Usually a build is converted into a root task or task group. =head2 RUNNER L A runner takes the composed tasks and executes them in the proper order. =head2 TASK L A task is a unit of work to accomplish. There are 2 main types of task. =head3 ACTION An action is the most simple unit used in composition. An action is essentially a name and a codeblock to run. =head3 GROUP A group is a task that is composed of other tasks. =head1 EXPORTS All exports are optional, you must request the ones you want. =over 4 =item $parsed = parse_args(args => \@args) =item $parsed = parse_args(args => \@args, level => $L) =item $parsed = parse_args(args => \@args, caller => [caller($L)]) This will parse a "typical" task builders arguments. The C<@args> array MUST contain a name (plain scalar containing text) and also a single CODE reference. The C<@args> array MAY also contain any quantity of line numbers or hashrefs. The resulting data structure will be a single hashref with all the provided hashrefs squashed together, and the 'name', 'code', 'lines' and 'frame' keys set from other arguments. { # All hashrefs from @args get squashed together: %squashed_input_hashref_data, # @args must have exactly 1 plaintext scalar that is not a number, it # is considered the name: name => 'name from input args' # Integer values are treated as line numbers lines => [ 35, 44 ], # Exactly 1 coderef must be provided in @args: code => \&some_code, # 'frame' contains the 'caller' data. This may be passed in directly, # obtained from the 'level' parameter, or automatically deduced. frame => ['A::Package', 'a_file.pm', 42, ...], } =item $build = init_root($pkg, %args) This will initialize (or return the existing) a build for the specified package. C<%args> get passed into the L constructor. This uses the following defaults (which can be overridden using C<%args>): name => $pkg, flat => 1, iso => 0, async => 0, is_root => 1, Note that C<%args> is completely ignored if the package build has already been initialized. =item $build = root_build($pkg) This will return the root build for the specified package. =item $build = current_build() This will return the build currently at the top of the build stack (or undef). =item $build = build($name, \%params, sub { ... }) This will push a new build object onto the build stash then run the provided codeblock. Once the codeblock has finished running the build will be popped off the stack and returned. See C for details about argument processing. =back =head1 SEE ALSO =over 4 =item Test2::Tools::Spec L is an implementation of RSPEC using this library. =back =head1 SOURCE The source code repository for Test2-Workflow can be found at F. =head1 MAINTAINERS =over 4 =item Chad Granum Eexodist@cpan.orgE =back =head1 AUTHORS =over 4 =item Chad Granum Eexodist@cpan.orgE =back =head1 COPYRIGHT Copyright 2018 Chad Granum Eexodist7@gmail.comE. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See F =cut