#!/usr/bin/perl -w

use strict;
use warnings;
use TAP::Harness;
use Getopt::Long;
our $VERSION = '0.24';

Getopt::Long::Configure (qw(bundling));

my $opts = { psql => 'psql', color => 1 };

Getopt::Long::GetOptions(
    'psql-bin|b=s'        => \$opts->{psql},
    'dbname|d=s'          => \$opts->{dbname},
    'username|U=s'        => \$opts->{username},
    'host|h=s'            => \$opts->{host},
    'port|p=s'            => \$opts->{port},
    'pset|P=s%'           => \$opts->{pset},
    'runtests|r'          => \$opts->{runtests},
    'schema|s=s'          => \$opts->{schema},
    'match|x=s'           => \$opts->{match},
    'timer|t'             => \$opts->{timer},
    'color|c!'            => \$opts->{color},
    'formatter|f=s'       => \$opts->{formatter},
    'archive|a=s'         => \$opts->{archive},
    'verbose|v+'          => \$opts->{verbose},
    'help|H'              => \$opts->{help},
    'man|m'               => \$opts->{man},
    'version|V'           => \$opts->{version},
) or require Pod::Usage && Pod::Usage::pod2usage(2);

if ( $opts->{help} or $opts->{man} ) {
    require Pod::Usage;
    Pod::Usage::pod2usage(
        '-sections' => $opts->{man} ? '.+' : '(?i:(Usage|Options))',
        '-verbose'  => 99,
        '-exitval' => 0,
    )
}

if ($opts->{version}) {
    print 'pg_prove ', main->VERSION, $/;
    exit;
}

# --schema and --match assume --runtests.
$opts->{runtests} ||= !!($opts->{schema} || $opts->{match});

unless ($opts->{runtests} or @ARGV) {
    require Pod::Usage;
    Pod::Usage::pod2usage(
        '-message'  => "\nOops! You didn't say what test scripts to run or specify --runtests.\n",
        '-sections' => '(?i:(Usage|Options))',
        '-verbose'  => 99,
        '-exitval'  => 1,
    )
}

my @command = ($opts->{psql});

for (qw(username host port dbname)) {
    push @command, "--$_" => $opts->{$_} if defined $opts->{$_}
}

# We can't use --tuples-only because then it doesn't allow --pset to toggle it
# properly. :-(
push @command, qw(
    --no-psqlrc
    --no-align
    --quiet
    --pset pager=
    --pset tuples_only=true
    --set ON_ERROR_ROLLBACK=1
    --set ON_ERROR_STOP=1
);

if (my $pset = $opts->{pset}) {
    while (my ($k, $v) = each %{ $pset }) {
        push @command, '--pset', "$k=$v";
    }
}

# Figure out what tests to run.
my $tests;
if ($opts->{runtests}) {
    # We're just going to call `runtests()`.
    push @command, '--command';

    my @args;
    for my $key qw(schema match) {
        next unless $opts->{$key};
        (my $arg = $opts->{$key}) =~ s/'/\\'/g;
        # Gotta cast the arguments.
        push @args, "'$arg'::" . ($key eq 'schema' ? 'name' : 'text');
    }

    # This is the command we'll run.
    $tests = [[
        "SELECT * FROM runtests(" . join( ', ', @args ) . ');',
        'runtests(' . join(', ', @args) . ')',
    ]];
} else {
    # Each item in @ARGV is a file name.
    $tests = \@ARGV;
    push @command, '--file';
}

# Are we using TAP::Harness or TAP::Harness::Archive?
my $harness_class = 'TAP::Harness';
if ( $opts->{archive} ) {
    require TAP::Harness::Archive;
    $harness_class = 'TAP::Harness::Archive';
}

my $verbosity = $opts->{verbose} || $ENV{TEST_VERBOSE};

# Make it so!
my $return = $harness_class->new({
    verbosity => $verbosity,
    failures  => !$verbosity,
    ( TAP::Harness->VERSION ge '3.17' ? (comments  => !$verbosity) : ()),
    timer           => $opts->{timer},
    color           => $opts->{color},
    formatter_class => $opts->{formatter},
    ($opts->{archive} ? (archive => $opts->{archive}) : ()),
    exec            => \@command,
})->runtests( @{ $tests } );

# Exit with non-zero status if tests failed.
exit 1 if $return->{failed};

__END__

=encoding utf8

=head1 Name

pg_prove - A command-line tool for running and harnessing pgTAP tests

=head1 Usage

  pg_prove -d template1 test*.sql
  pg_prove -d testdb -s testschema

=head1 Description

C<pg_prove> is a command-line application to run one or more pgTAP tests in a
PostgreSQL database. The output of the tests is harvested and processed by
L<TAP::Harness|TAP::Harness> in order to summarize the results of the test.

Tests can be written and run in one of two ways, as SQL scripts or as database
functions.

=head2 Test Scripts

pgTAP test scripts should consist of a series of SQL statements that output
TAP. Here’s a simple example that assumes that the pgTAP functions have been
installed in the database:

    -- Start transaction and plan the tests.
    BEGIN;
    SELECT plan(1);

    -- Run the tests.
    SELECT pass( 'My test passed, w00t!' );

    -- Finish the tests and clean up.
    SELECT * FROM finish();
    ROLLBACK;

Now run the tests by passing the list of SQL script names to C<pg_prove>.
Here’s what it looks like when the pgTAP tests are run with C<pg_prove>

    % pg_prove -U postgres sql/*.sql
    sql/coltap.....ok
    sql/hastap.....ok
    sql/moretap....ok
    sql/pg73.......ok
    sql/pktap......ok
    All tests successful.
    Files=5, Tests=216,  1 wallclock secs ( 0.06 usr  0.02 sys +  0.08 cusr  0.07 csys =  0.23 CPU)
    Result: PASS

=head2 xUnit Test Functions

pgTAP test functions should return a set of text, and then simply return the
values returned by pgTAP functions, like so:

    CREATE OR REPLACE FUNCTION setup_insert(
    ) RETURNS SETOF TEXT AS $$
        RETURN NEXT is( MAX(nick), NULL, 'Should have no users') FROM users;
        INSERT INTO users (nick) VALUES ('theory');
    $$ LANGUAGE plpgsql;

    CREATE OR REPLACE FUNCTION test_user(
    ) RETURNS SETOF TEXT AS $$
        SELECT is( nick, 'theory', 'Should have nick') FROM users;
    END;
    $$ LANGUAGE sql;

Once you have these functions defined in your database, you can run them with
C<pg_prove> by using the C<--runtests> option.

    % pg_prove -d myapp -r
    runtests()....ok
    All tests successful.
    Files=1, Tests=32,  0 wallclock secs ( 0.02 usr  0.01 sys +  0.01 cusr  0.00 csys =  0.04 CPU)
    Result: PASS

Be sure to pass the C<--schema> option if your test functions are all in one
schema, and the C<--match> option if they have names that don’t start with
“test”. For example, if you have all of your test functions in “test” schema
and I<ending> with “test,” run the tests like so:

    pg_prove -d myapp --schema test --match 'test$'

=head1 Options

  -b --psql-bin PSQL        Location of the psql program.
  -d --dbname DBNAME        Database to which to connect.
  -U --username USERNAME    Username with which to connect.
  -h --host HOST            Host to which to connect.
  -p --port PORT            Port to which to connect.
  -P --pset OPTION=VALUE    Set psql printing option.
  -v --verbose              Display output of test scripts while running them.
  -r --runtests             Run xUnit test using C<runtests()>.
  -s --schema               Schema in which to find xUnit tests.
  -x --match                Regular expression to find xUnit tests.
  -t --timer                Print elapsed time after each test file.
  -c --color                Display colored test ouput.
     --nocolor              Do not display colored test ouput.
  -f --formatter FORMATTER  TAP::Formatter class to format output.
  -a --archive FILENAME     Store the resulting TAP in an archive file.
  -H --help                 Print a usage statement and exit.
  -m --man                  Print the complete documentation and exit.
  -V --version              Print the version number and exit.

=head1 Options Details

=over

=item C<-b>

=item C<--psql-bin>

  pg_prove --psql-bin /usr/local/pgsql/bin/psql
  pg_prove -b /usr/local/bin/psql

Path to the C<psql> program, which will be used to actually run the tests.
Defaults to F<psql>.

=item C<-d>

=item C<--dbname>

  pg_prove --dbname try
  pg_prove -d postgres

The name of database to which to connect. Defaults to the value of the
C<$PGDATABASE> environment variable or to the system username.

=item C<-U>

=item C<--username>

  pg_prove --username foo
  pg_prove -U postgres

PostgreSQL user name to connect as. Defaults to the value of the C<$PGUSER>
environment variable or to the operating system name of the user running the
application.

=item C<-h>

=item C<--host>

  pg_prove --host pg.example.com
  pg_prove -h dev.local

Specifies the host name of the machine on which the server is running. If the
value begins with a slash, it is used as the directory for the Unix-domain
socket. Defaults to the value of the C<$PGHOST> environment variable or
localhost.

=item C<-p>

=item C<--port>

  pg_prove --port 1234
  pg_prove -p 666

Specifies the TCP port or the local Unix-domain socket file extension on which
the server is listening for connections. Defaults to the value of the
C<$PGPORT> environment variable or, if not set, to the port specified at
compile time, usually 5432.

=item C<-P>

=item C<--pset>

  pg_prove --pset tuples_only=0
  pg_prove -P null=[NULL]

Specifies printing options in the style of C<\pset> in the C<psql> program.
See L<http://www.postgresql.org/docs/current/static/app-psql.html> for details
on the supported options.

=item C<-v>

=item C<--verbose>

  pg_prove --verbose
  pg_prove -v

Display standard output of test scripts while running them. This behavior can
also be triggered by setting the C<$TEST_VERBOSE> environment variable to a
true value.

=item C<-r>

=item C<--runtests>

  pg_prove --runtests
  pg_prove -r

Don’t run any test scripts, but just use the C<runtests()> pgTAP function to
run xUnit tests. This ends up looking like a single test script has been run,
when in fact no test scripts have been run. Instead, C<pg_prove> tells C<psql>
to run something like:

  psql --command 'SELECT * FROM runtests()'

You should use this option when you’ve written your tests in xUnit style,
where they’re all defined in test functions already loaded in the database.

=item C<-s>

=item C<--schema>

  pg_prove --schema test
  pg_prove -s mytest

Used with C<--runtests>, and, in fact, implicitly forces C<--runtests> to be
true. This option can be used to specify the name of a schema in which to find
xUnit functions to run. Basically, it tells C<psql> to run something like:

  psql --command "SELECT * FROM runtests('test'::name)"

=item C<-x>

=item C<--match>

  pg_prove --match 'test$'
  pg_prove -x _test_

Used with C<--runtests>, and, in fact, implicitly forces C<--runtests> to be
true. This option can be used to specify a POSIX regular expression that will
be used to search for xUnit functions to run. Basically, it tells C<psql> to
run something like:

  psql --command "SELECT * FROM runtests('_test_'::text)"

This will run any visible functions with the string “_test_” in their names.
This can be especially useful if you just want to run a single test in a
given schema. For example, this:

  pg_prove --schema testing --match '^test_widgets$'

Will have C<psql> execute the C<runtests()> function like so:

 SELECT * FROM runtests('testing'::name, '^test_widgets$'::text);

=item C<-t>

=item C<--timer>

  pg_prove --timer
  pg_prove -t

Print elapsed time after each test file.

=item C<-t>

=item C<--color>

  pg_prove --color
  pg_prove -c

Display test results in color. Colored test output is the default, but if
output is not to a terminal, color is disabled.

Requires L<Term::ANSIColor|Term::ANSIColor> on Unix-like platforms and
L<Win32::Console|Win32::Console> on Windows. If the necessary module is not
installed colored output will not be available.

=item C<--nocolor>

Do not display test results in color.

=item C<-f>

=item C<--formatter>

  pg_prove --formatter TAP::Formatter::File
  pg_prove -f TAP::Formatter::Console

The name of the class to use to format output. The default is
L<TAP::Formatter::Console|TAP::Formatter::Console>, or
L<TAP::Formatter::File|TAP::Formatter::File> if the output isn’t a TTY.

=item C<-a>

=item C<--archive>

  pg_prove --archive tap.tar.gz
  pg_prove -a test_output.tar

Send the TAP output to a TAP archive file as wel as to the normal output
destination. The archive formats supported are F<.tar> and F<.tar.gz>.

=item C<-H>

=item C<--help>

  pg_prove --help
  pg_prove -H

Outputs a brief description of the options supported by C<pg_prove> and exits.

=item C<-m>

=item C<--man>

  pg_prove --man
  pg_prove -m

Outputs this documentation and exits.

=item C<-V>

=item C<--version>

  pg_prove --version
  pg_prove -V

Outputs the program name and version and exits.

=back

=head1 Author

David E. Wheeler <david@kineticode.com>

=head1 Copyright

Copyright (c) 2008-2010 Kineticode, Inc. Some Rights Reserved.

=cut
