The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package # hidden from PAUSE indexer
Test::Expr;
our $VERSION = '0.000001';

use 5.012; use warnings; use autodie;
use Keyword::Declare;
use Test::More;
use Data::Dump;
use List::Util 'max';
use parent 'Exporter';
our @EXPORT = @Test::More::EXPORT;
use PPI;

sub _trim {
    my $str = shift;
    $str =~ s{\A\s*|\s*\Z}{}g;
    return $str;
}

sub import {
    my ($package) = @_;
    $package->export_to_level(1, @_);

    keyword test (Expr $expr) {
        # Break the test on the first of these...
        state $COMPARATOR = qr{\A (?: == | != | < | > | <= | >= | eq | ne | lt | gt | le | ge ) \Z}xms;

        # Decompose the test arguments...
        $expr = PPI::Document->new(\$expr);
        my %arg;
        my $curr = 'found';
        for my $component ($expr->child(0)->children) {
            if ($curr eq 'found' && $component =~ $COMPARATOR) {
                $arg{comparator} = $component;
                $curr = 'expected';
            }
            elsif ($curr eq 'expected' && $component eq '=>') {
                $curr = 'desc';
            }
            else {
                $arg{$curr} .= $component;
            }
        }

        # Tidy up any loose ends...
        $arg{desc} //= qq{q{$expr at }.__FILE__.q{ line }.__LINE__};
        $_ = _trim($_) for values %arg;

        # Work out what values to report if there's a problem...
        my @vars;
        for my $sym (@{$expr->find('PPI::Token::Symbol')}) {
            push @vars, $sym;
            my $sib = $sym;
            while (( $sib = $sib->snext_sibling )) {
                last if not $sib->isa('PPI::Structure::Subscript');
                $vars[-1] .= $sib;
            }
        }
        my $var_len = max map {length} @vars;
        my %seen;
        my @diagnostics
            = map {qq{Test::More::diag sprintf(q{    %${var_len}s --> }, q{$_}), Data::Dump::dump($_);}}
                  grep { !$seen{$_}++ }
                       @vars;

        # Build the test code...
        qq{ if ($arg{found} $arg{comparator} $arg{expected}) {
                Test::More::ok 1, $arg{desc};
            }
            else {
                Test::More::fail $arg{desc};
                Test::More::diag q{  $arg{found} !$arg{comparator} $arg{expected}};
                Test::More::diag q{  because:}; @diagnostics
                Test::More::diag "";
            }
        } =~ s{\n}{}gr;
    }
}

1;