package swat;
our $VERSION = v0.1.12;
1;

package main;
use strict;
use Test::More;
our $HTTP_RESPONSE;
our ($curl_cmd, $content_file, $url, $path, $http_meth, $debug, $ignore_http_err, $try_num, $debug_bytes );
our ($a, $b);

$| = 1;

sub execute_with_retry {

    my $cmd = shift;
    my $try = shift || 1;

    for my $i (1..$try){
        diag "\nexecute cmd: $cmd, attempt number: $i" if $debug;
        return $i if system($cmd) == 0;
        sleep $i**2;
    }
    return

}

sub make_http_request {

    return $HTTP_RESPONSE if defined $HTTP_RESPONSE;
    my $st = execute_with_retry("$curl_cmd > $content_file && test -s $content_file", $try_num);

    open F, $content_file or die $!;
    $HTTP_RESPONSE = '';
    $HTTP_RESPONSE.= $_ while <F>;
    close F;

    diag `head -c $debug_bytes $content_file` if $debug;

    ok($st, "successfull response from $http_meth $url$path") unless $ignore_http_err;

    diag "data file: $content_file";

    return $HTTP_RESPONSE;
}

sub hostname {
    my $a = `hostname`;
    chomp $a;
    return $a;
}

sub check_line {
 
    my $pattern = shift;
    my $check_type = shift;
    my $message = shift;

    my $status = 0;
    my $data = make_http_request();

    my @chunks;

    if ($check_type eq 'default'){
        $status = 1 if index($data,$pattern) != -1
    }elsif($check_type eq 'regexp'){
        for my $l (split /\n/, $data){
            chomp $l;
            if ($l =~ qr/$pattern/){
                push @chunks, $1||$&;
                $status = 1;
            }
        }
    }else {
        die "unknown check_type: $check_type";
    }

    ok($status,$message);


    for my $c (@chunks){
        diag("line found: $c");
    }

    return

}


sub header {

    diag("start swat for $url/$path");
    diag("debug $debug | try num $try_num | ignore http errors $ignore_http_err")
}

sub generate_asserts {

    header();

    my $patterns_file = shift;

    open my $fh, $patterns_file or die $!;

    my $comment;

    while (my $l = <$fh>){

        chomp $l;
        

        next unless $l =~ /\S/; # skip blank lines
        if ($l=~ /^\s*#(.*)/) { # skip comments
            $comment = $1;
            s/^\s+//, s/\s+$// for $comment;
            next;
        }

        if ($l=~/\s*code:\s+(.*)/){
            undef $comment;
            my $code = $1;
            eval $code;            
        }elsif($l=~/\s*regexp:\s+(.*)/){
            my $re=$1;
            my $message = $comment ? "[$comment] $http_meth $path returns data matching $re" : "$http_meth $path returns data matching $re";
            check_line($re, 'regexp', $message);
            undef $comment;
        }else{
            s{#.*}[], s{\s+$}[], s{^\s+}[] for $l;
            my $message = $comment ? "[$comment] $http_meth $path returns $l" : "$http_meth $path returns $l";
            check_line($l, 'default', $message);
            undef $comment;
        }
    }

    close $fh;

}



1;


=head1 SYNOPSIS

SWAT is Simple Web Application Test ( Tool )

    $  swat examples/google/ google.ru
    /home/vagrant/.swat/reports/google.ru/00.t ..
    # start swat for google.ru//
    # try num 2
    ok 1 - successfull response from GET google.ru/
    # data file: /home/vagrant/.swat/reports/google.ru///content.GET.txt
    ok 2 - GET / returns 200 OK
    ok 3 - GET / returns Google
    1..3
    ok
    All tests successful.
    Files=1, Tests=3, 12 wallclock secs ( 0.00 usr  0.00 sys +  0.02 cusr  0.00 csys =  0.02 CPU)
    Result: PASS


=head1 WHY

I know there are a lot of tests tool and frameworks, but let me  briefly tell I<why> I created swat.
As devops I update a dozens of web application weekly, sometimes I just have I<no time> sitting and wait while dev guys or QA team ensure that deploy is fine and nothing breaks on the road. So I need a B<tool to run smoke tests against web applications>. Not tool only, but the way to B<create such a tests from the scratch in way easy and fast enough>. So this how I came up with the idea of swat. If I was a marketing guy I'd say that swat:

=over

=item -

is easy to use and flexible tool to run smoke tests against web applications


=item -

is L<curl|http://curl.haxx.se/> powered and L<TAP|https://testanything.org/> compatible


=item -

has minimal dependency tree  and probably will run out of the box on most linux environments, provided that one has perl/bash/find/curl by hand ( which is true  for most cases )


=item -

has a simple and yet powerful DSL allow you to both run simple tests ( 200 OK ) or complicated ones ( using curl api and perl one-liners calls )


=item -

is daily it/devops/dev helper with low price mastering ( see my tutorial )


=item -

and yes ... swat is fun :)


=back


=head1 Tutorial


=head2 Install swat


=head2 developer release

    sudo cpanm --mirror-only --mirror https://stratopan.com/melezhik/swat-release/master swat


=head2 stable release

    sudo cpan install swat

Once swat is installed you have swat command line tool to run swat tests, but before do this you need to create them.


=head2 Create tests

    mkdir  my-app/ # create a project root directory to hold tests

    # define http URIs application should response to

    mkdir -p my-app/hello # GET /hello
    mkdir -p my-app/hello/world # GET /hello/world

    # define the content the expected to return by requested URIs

    echo 200 OK >> my-app/hello/get.txt
    echo 200 OK >> my-app/hello/world/get.txt

    echo 'This is hello' >> my-app/hello/get.txt
    echo 'This is hello world' >> my-app/hello/world/get.txt


=head2 Run tests

    swat ./my-app http://127.0.0.1


=head1 DSL

Swat DSL consists of 2 parts. Routes and check patterns.


=head2 Routes

Routes are http resources a tested web application should has.

Swat utilize file system data calculating all existed routes as sub directories paths in the project root directory.
Let we have a following project layout:

    example/my-app/
    example/my-app/hello/
    example/my-app/hello/get.txt
    example/my-app/hello/world/get.txt

When you give swat a run

    swat example/my-app 127.0.0.1

It just find all the directories holding get.txt files and "create" routes:

    GET hello/
    GET hello/world

Then check patterns come into play.


=head2 Check patterns

As you can see from tutorial above check patterns are  just text files describing B<what> is expected to return when route requested. Check patterns file parsed by swat line by line and take an action depending on entity found. There are 3 types of entities may be found in check patterns file:

=over

=item -

Expected Values


=item -

Comments


=item -

Perl one-liners code


=back


=head3 Expected values

This is most usable entity that one may define at check patterns files. I<It's just s string should be returned> when swat request a given URI. Here are examples:

    200 OK
    Hello World
    <head><title>Hello World</title></head>


=head3 Comments

Comments are lines started with '#' symbol, they are for human not for swat which ignore them when parse check pattern file. Here are examples.

    # this http status is expected
    200 OK
    Hello World # this string should be in the response
    <head><title>Hello World</title></head> # and it should be proper html code


=head3 Perl one-liners code

Everything started with C<code:> would be treated by swat as perl code to execute.
There are a I<lot of> possibilities! Please follow L<Test::More|search.cpan.org/perldoc/Test::More> documentation to get more info about useful function you may call here.

    code: skip('next test is skipped',1) # skip next check forever
    HELLO WORLD


=head3 Using regexp

Regexps are subtypes of expected values, with the only adjustment that you may use I<perl regular expressions> instead of plain strings checks.
Everything started with C<regexp:> would be treated as regular expression.

    # this is example of regexp check
    regexp: App Version Number: (\d+\.\d+\.\d+)


=head1 Post requests

When talking about swat I always say about Get http request, but swat may send a Post http request just name your check patterns file  as post.txt instead of get.txt

    echo 200 OK >> my-app/hello/post.txt
    echo 200 OK >> my-app/hello/world/post.txt

You may use curl_params settings ( follow swat settings section for details ) to define post data, there are examples:

=over

=item -

C<-d> - Post data sending by html form submit.


=back


     # Place this in swat.ini file or sets as env variable:
     curl_params='-d name=daniel -d skill=lousy'


=over

=item -

C<--data-binary> - Post data sending as is.


=back


     # Place this in swat.ini file or sets as env variable:
     curl_params=`echo -E "--data-binary '{\"name\":\"alex\",\"last_name\":\"melezhik\"}'"`
     curl_params="${curl_params} -H 'Content-Type: application/json'"



=head1 Swat settings

Swat comes with settings defined in two contexts:

=over

=item -

environmental variables


=item -

swat.ini files


=back


=head2 Environmental variables

Defining a proper environment variables will provide swat settings.

=over

=item -

debug - set to 1 if you want to see some debug information in output, default value is C<0>


=item -

debug_bytes - number of bytes of http response  to be dumped out when debug is on. default value is C<500>


=item -

ignoreI<http>err - ignore http errors, if this parameters is off (set to C<1>) returned  I<error http codes> will not result in test fails, useful when one need to test something with response differ from  2**,3** http codes. Default value is C<0>


=item -

try_num - number of http requests  attempts before give it up ( useless for resources with slow response  ), default value is C<2>


=item -

curl_params - additional curl parameters being add to http requests, default value is C<"">, follow curl documentation for variety of values for this


=item -

curlI<connect>timeout - follow curl documentation


=item -

curlI<max>time - follow curl documentation


=item -

port  - http port of tested host, default value is '80'


=back


=head2 Swat.ini files

Swat checks files named C<swat.ini> in the following directories
- ~/swat.ini
- $project_root_directory/swat.ini
- $route_directory/swat.ini

Here are examples of locations of swat.ini files:


     ~/swat.ini # home directory swat.ini file
     my-app/swat.ini # project_root directory swat.ini file
     my-app/hello/get.txt
     my-app/hello/swat.ini # route directory swat.ini file ( route hello )
     my-app/hello/world/get.txt
     my-app/hello/world/swat.ini # route directory swat.ini file ( route hello/world )


Once file exists at ay location swat simply B<bash source it> to apply settings

Thus swat.ini file should be bash file with swat variables definitions. Here is example:

    # the content of swat.ini file:
    curl_params="-H 'Content-Type: text/html'"
    debug=1


=head2 Settings priority table

Here is the list of settings/contexts  in priority ascending order:

| context | location | priority  level |
| --------| ----- | --------- |---- |
| swat.ini file           | ~/swat.ini              | 1 |
| environmental variables | ---                     | 2 |
| swat.ini file           | project root directory  | 3 |
| swat.ini file           | route directory         | 4 |

Swat process settings in order. For every route found swat:
- Clear all settings
- Apply settings from environmental variables ( if any given )
- Apply settings from swat.ini file in home directory ( if any given )
- Apply settings from swat.ini file in project root directory ( if any given )
- And finally apply settings from swat.ini file in route directory ( if any given )


=head1 TAP

Swat produce output in L<TAP|https://testanything.org/> format , that means you may use your favorite tap parsers to bring result to
another test / reporting systems, follow TAP documentation to get more on this.


=head1 Command line tool

Swat is shipped as cpan package , once it's installed ( see install section ) you have a command line tool called C<swat>, this is usage info on it:

    swat project_dir URL <options>

=over

=item -

URL - is base url for web application you run tests against, you need defined routes which will be requested against URL, see DSL section.


=item -

project_dir - is a project root directory


=back


=head2 options

As swat I<uses prove utility> to run tests, all the swat options are passed as is to prove utility.
Follow L<prove|http://search.cpan.org/perldoc?prove> utility documentation for variety of values you may set here.
Default value for options is  C<-v>. Here is another examples:

=over

=item -

C<-q -s> -  run tests in random and quite mode


=back


=head1 Examples

./examples directory contains examples of swat tests for different cases. Follow README.md files for details.


=head1 Dependencies

Not that many :)

=over

=item -

perl / curl / bash / find  / head


=back


=head1 AUTHOR

L<Aleksei Melezhik|mailto:melezhik@gmail.com>


=head1 Thanks

To the authors of ( see list ) without who swat would not appear to light
- perl
- curl
- TAP
- Test::More
- prove

