This is a reasonable TAP parser in 40 lines of code:
sub tap_failed { my $tap = shift; my $plan_re = qr/1\.\.(\d+)/; my $test_re = qr/(?:not )?ok/; my @failed; my $core_tap = ''; foreach ( split "\n" => $tap ) { if ( /^not ok/ ) { # TODO tests are not failures push @failed => $_ unless m/^ ( [^\\\#]* (?: \\. [^\\\#]* )* ) \# \s* TODO \b \s* (.*) $/ix } next unless /^(?:$plan_re|$test_re)/; $core_tap .= "$_\n"; } my $plan; if ( $core_tap =~ /^$plan_re/ ) { $plan = $1; } elsif ( $core_tap =~ /$plan_re$/ ) { $plan = $1; } return 'No plan found' unless defined $plan; if ( @failed ) { my $failed = @failed; return "Failed $failed out of $plan tests"; } my $plans_found = 0; $plans_found++ while $core_tap =~ /^$plan_re/gm; return '$plans_found plans found' if $plans_found > 1; my $tests = 0; $tests++ while $core_tap =~ /^$test_re/gm; return "Planned $plan tests and found $tests tests" if $tests != $plan; return; }
OK, it's not really a parser. It only tells you if the tests failed or passed. There's tons of information which it discards and it does virtually no validation of the TAP structure, but might solve the nested TAP problem.
Returns codes can be a legitimate concern, but in many cases these are unavailable anyway. Also, context does not change whether or not a test succeeded or failed, it changes the interpretation of whether it succeeded or failed. However, if something fails, it's better to default to failure in the absence of more information.
As for the v14/v15 issue, if we guarantee that TAP core doesn't change, we're safe. This entire work is designed to eliminate the need to break backwards-compatibility. I think we were far too rushed to make such a decision lightly.