Group
Extension

Tapper-API/lib/Tapper/API/Plugin/Integrationtest.pm

package Tapper::API::Plugin::Integrationtest;
our $AUTHORITY = 'cpan:TAPPER';
# ABSTRACT: API functions for integration tests
$Tapper::API::Plugin::Integrationtest::VERSION = '5.0.2';
use warnings;
use strict;
use 5.010;

use Mojolicious::Lite;
use JSON::XS;
use Tapper::Cmd::Testplan;
use Tapper::Cmd::Host;
use Tapper::Cmd::Testrun;
use Tapper::Model 'model';
use File::ShareDir;
use File::Spec;
use Storable;
use File::Temp 'tempfile';

plugin('RenderFile');

my $cfg      = Tapper::Config->subconfig;
my %tests = (
             "example1" => {groups => ["ALL"], timeout => 8000},
             "example2" => {groups => ["ALL"], timeout => 3600*24*2},
            );


sub get_groups
{
        my %groups;
        foreach my $test (keys %tests) {
                foreach my $group (@{$tests{$test}->{groups} || []}) {
                        $groups{$group} //= [];
                        push @{$groups{$group}}, {name => $test, %{$tests{$test}}};
                }
        }
        return \%groups;
}

sub _get_api_base {
    my $base_url = '';

    # package name full or just last part
    my $plugin_name1 = __PACKAGE__;
    my $plugin_name2 = $plugin_name1;
    $plugin_name2    =~ s/^.*:://;

    return unless $cfg->{api}{routes};

    foreach (@{$cfg->{api}{routes}}) {
        if ($_->{module} eq $plugin_name1 or $_->{module} eq $plugin_name2) {
            $base_url = $_->{url};
            last;
        }
    }
    return $base_url;
}

put 'host-new/room/:room/ip/:ip' => sub {
        my $self = shift;
        my $room = $self->param('room');
        my $ip = $self->param('ip');
        $self->render(json => {id => 12}, status => 201);
};

put 'host-new/name/:name' => sub {
        my $self = shift;
        my $name = $self->param('name');
        my ($room, $ip) = split('-', $name, 2);  # if multiple dashes are in $name, they end up in $ip
        $self->render(json => {id => 12}, status => 201);
};

put 'testrun-start/:testrun' => sub {
        my $self = shift;
        $self->app->renderer->default_format('json');


        my $data;
        if ($self->tx->req->body) {
                $data = eval{JSON::XS::decode_json($self->tx->req->body)};
                if ($@) {
                        $self->render(json => {
                                               error => $@
                                              },
                                      status => 409,
                                     );
                        return;
                }
        }

        if ($data->{group}) {
                my $tests = get_groups()->{$data->{group}};
                push @{$data->{tests}}, @$tests;
        }

        my $test = $self->param('testrun');
        my $test_filename = File::Spec->catfile($cfg->{paths}->{use_case_path},"$test.sc");
        if (not -r $test_filename) {
                $test_filename = File::Spec->catfile($cfg->{paths}->{use_case_path},"$test.mpc");
                if (not -r $test_filename) {
                        my $error = "Tried to find testrun description in ";
                          $error .= File::Spec->catfile($cfg->{paths}->{use_case_path},"$test.sc");
                          $error .= " and $test_filename. Neither was readable.";
                        $self->respond_to(json => {json => {error => $error}, status => 409,},
                                          any  => {text => "Error - $error", status => 409,},
                                 );
                return;

                }
        }
        my $cmd = Tapper::Cmd::Testrun->new();

        my $test_string = eval{$cmd->apply_macro($test_filename, $data)};
        if ($@) {
                $self->respond_to(json => {json => {error => "$@"}, status => 409,},
                                  any  => {text => "Error - $@", status => 409,},
                                 );
                return;
        }
        if ($data->{dryrun}) {
                $self->render(text => $test_string,
                              status => 202,
                             );
                return;
        }

        my @ids = eval {
                        require YAML::Syck;
                        my @plan = YAML::Syck::Load($test_string);
                        if ($plan[0]->{scenario_type}) {
                                require Tapper::Cmd::Scenario;
                                return Tapper::Cmd::Scenario->new()->add(\@plan); # returns from eval block
                        }
                        # we have a plain precondition description without additional testrun description
                        # need to add options that $cmd->create needs
                        elsif (not $plan[0]->{preconditions}) {
                                # don't use \@plan, since we overwrite it in the next step
                                my $tmp_plan = { preconditions => [ @plan ], topic => uc($test) };
                                @plan = ($tmp_plan);
                        }
                        $cmd->create($plan[0]);
        };
        if ($@) {
                $self->respond_to(json => {json => {error => "$@"}, status => 409,},
                                  any  => {text => "Error - $@", status => 409,},
                                 );
                return;
        }
        my $text_return;
        foreach my $id (@ids) {
                $text_return .= "url: $cfg->{base_url}/testrun/id/$id\n";
        }
        $self->respond_to(json => {json => {testrun => {
                                                         ids => [ @ids ],
                                                         links => [ map {"$cfg->{base_url}/testruns/id/$_"} @ids ],
                                                        },
                                           }, status => 202,
                                  },
                          any  => {text => $text_return, status => 202,},
                         );

};


put 'testplan-start/:testplan' => sub {
        my $self = shift;
        $self->app->renderer->default_format('json');


        my $data;
        if ($self->tx->req->body) {
                $data = eval{JSON::XS::decode_json($self->tx->req->body)};
                if ($@) {
                        $self->render(json => {
                                               error => $@
                                              },
                                      status => 409,
                                     );
                        return;
                }
        }

        # if host does not yet exist, create it and bind it to integrationtest queue
        if ($data->{hosts} and $data->{room}) {
        HOST:
                foreach my $host (@{$data->{hosts}}) {
                        my $room = $data->{room};
                        if (not $host =~ /$room-/i) {
                                $host = join("-",lc($room),$host);
                        }

                        my $host_r = Tapper::Model::model()->resultset('Host')->search({name => $host}, {rows => 1})->first;
                        if ($host_r) {
                                # hosts already in DB, activate it if needed
                                if (not $host_r->active) {
                                        $host_r->active(1);
                                        $host_r->update;
                                }
                                next HOST;
                        }
                        # host not in DB, create it
                        my $cmd = Tapper::Cmd::Host->new();
                        $cmd->add({name => $host,
                                   active => 1,
                                   free => 1,
                                   comment => '(autoadded by Tapper::API)'});

                        $host_r = Tapper::Model::model()->resultset('Host')->search({name => $host}, {rows => 1})->first;
                        my $queue_r = Tapper::Model::model('TestrunDB')->resultset('Queue')->search({name => 'integration'}, {rows => 1})->first;

                        # don't bind twice
                        next HOST if $host_r->queuehosts->search({queue_id => $queue_r->id}, {rows => 1})->first;
                        Tapper::Model::model('TestrunDB')->resultset('QueueHost')->new({queue_id => $queue_r->id,
                                                                                        host_id  => $host_r->id,
                                                                                       })->insert;

                }
        }

        my $cmd = Tapper::Cmd::Testplan->new();
        $data->{tests} //= [];

        if ($data->{group}) {
                my $tests = get_groups()->{$data->{group}};
                push @{$data->{tests}}, @$tests;
        }

        my $testplan = $self->param('testplan');
        my $plan_filename = File::Spec->catfile($cfg->{paths}->{testplan_path},
                                                lc((split(/::/,__PACKAGE__))[-1]), # last part of package name
                                                "$testplan.tp");

        my $safe_data=Storable::dclone($data); # make deep copy of substitute because TT changes them
        my $plan_evaluated = eval {$cmd->apply_macro($plan_filename, $data)};

        $data->{title} ||= $cmd->get_shortname($plan_evaluated);
        $data->{title} ||= $testplan;
        $data = $safe_data;

        if ($data->{dryrun}) {
                if ($@) {
                        $self->respond_to(json => {json => {error => "$@"}, status => 409,},
                                          any  => {text => "Error - $@", status => 409,},
                                         );
                } else {
                        $self->render(text => $plan_evaluated,
                                      status => 202,
                                     );
                }
                return;
        }

        my $id = eval {
                $cmd->testplannew(
                                  {
                                   file => $plan_filename,
                                   substitutes => $data,
                                   name => $data->{title},
                                  });
        };
        if ($@) {
                $self->respond_to(json => {json => {error => "$@"}, status => 409,},
                                  any  => {text => "Error - $@", status => 409,},
                                 );
                return;
        }
        my $tp = model('TestrunDB')->resultset('TestplanInstance')->find($id);
        my @testplans = map { {id => $_->id, link => "$cfg->{base_url}/testruns/id/".$_->id }} $tp->testruns->all;
        $self->respond_to(json => {json => {testplan => {
                                                         id => $id,
                                                         link => "$cfg->{base_url}/testplan/id/$id",
                                                        },
                                            testrun  => \@testplans,
                                           }, status => 202,
                                  },
                          any  => {text => "Text - url: $cfg->{base_url}/testplan/id/$id\n", status => 202,},
                         );

};


get 'query/testplan-status/id/:testplan' => sub {
        my $self = shift;
        my $testplan = $self->param('testplan');
        my $cmd = Tapper::Cmd::Testplan->new();
        my $result = $cmd->status($testplan);
        $result->{link} = "$cfg->{base_url}/testplan/id/$testplan",
        $self->render(json => $result,
                      status => 202,
                     );
};

get 'query/testrun-status/id/:testrun' => sub {
        my $self = shift;
        my $testrun = $self->param('testrun');
        my $cmd = Tapper::Cmd::Testrun->new();
        my $result = $cmd->status($testrun);
        $result->{link} = "$cfg->{base_url}/testrun/id/$testrun",
        $self->render(json => $result,
                      status => 202,
                     );
};


put 'testrun-cancel/id/:testrun' => sub {
        my $self = shift;
        my $testrun = $self->param('testrun');
        $self->render(json => {
                               $testrun => {
                                            success => 'error',
                                            'error-msg' => 'You are not allowed to cancel this testrun'
                                           },
                              },
                      status => 202,
                     );
};

put 'testplan-cancel/id/:testplan' => sub {
        my $self = shift;
        my $testplan = $self->param('testplan');
        $self->render(json => {
                               success => 'canceled',
                               'error-msg' => undef,
                              },
                      status => 202,
                     );
};


get 'query/testplan-list/' => sub {
        my $self = shift;
        $self->render(json => {
                               collection => {
                                              options => ['room', 'group', 'tests', 'hosts']
                                             }
                              },
                      status => 202,
                     );
};


get 'query/test-list/' => sub {
        my $self = shift;
        $self->render(json => \%tests,
                      status => 202,
                     );
};

any 'query/report-filelist/id/:report_id' => sub {
        my $self = shift;
        $self->app->renderer->default_format('json');

        my $report_id = $self->param('report_id');
        my $filter_filename = $self->param('filter_filename');
        my $file_result;
        if ($filter_filename) {
                $file_result = model->resultset('ReportFile')->search({report_id => $report_id, filename => {'like' => $filter_filename}});
        } else {
                $file_result = model->resultset('ReportFile')->search({report_id => $report_id});
        }

        $self->respond_to(
                          json => sub { $self->render(json =>  {map {$_->id => $_->filename} $file_result->all} )},
                          html => sub {
                                  my $file_ids = [map {$_->id} $file_result->all];
                                  $self->stash(file_ids => $file_ids, api_base => _get_api_base());
                                  $self->render(template => 'queryreportfilelistidreport_id')
                          }
                         );
};

any 'query/report-list/name/:report_name/limit/:limit' => sub {
        my $self = shift;
        $self->app->renderer->default_format('json');

        my $report_name = $self->param('report_name') || '';
        my $limit       = $self->param('limit') || 1;
        my $file_result;
        if ($report_name) {
                $file_result = model->resultset('ReportFile')->search({filename => {'like' => $report_name}}, {limit => $limit});
        }

        $self->respond_to(
                          json => sub { $self->render(json =>  {map {$_->id => $_->filename} $file_result->all} )},
                          html => sub {
                                  my $file_ids = [map {$_->id} $file_result->all];
                                  $self->stash(file_ids => $file_ids, api_base => _get_api_base());
                                  $self->render(template => 'queryreportfilelistidreport_id')
                          }
                         );
};


any 'query/testrun-filelist/id/:testrun_id' => sub {
        my $self = shift;
        $self->app->renderer->default_format('html');

        my $cmd_testrun = Tapper::Cmd::Testrun->new();
        my $testrun_id = $self->param('testrun_id');
        my $reports_rs = model->resultset('ReportgroupTestrun')->search({testrun_id => $testrun_id});
        my @report_ids = map {$_->report_id} $reports_rs->all;
        $self->stash(report_ids => \@report_ids, api_base => _get_api_base());
};

any 'query/testplan-filelist/id/:testplan_id' => sub {
        my $self = shift;
        $self->app->renderer->default_format('html');

        my $testplan_id = $self->param('testplan_id');
        my $filter = $self->param('filter_filename');
        $filter ||= '%';

        my $cmd = Tapper::Cmd::Testplan->new();
        my $file_ids = $cmd->testplan_files($testplan_id, $filter);
        $self->stash(file_ids => $file_ids, api_base => _get_api_base());
};

any 'query/reportfile/*filepath' => sub {
        my $self = shift;
        $self->app->renderer->default_format('html');

        my $filepath = $self->param('filepath');
        my $file_id = (split '/',$filepath)[-1];
        my $reportfile_result = model->resultset('ReportFile')->find($file_id);
        if (not $reportfile_result) {
                $self->render(text => "No file with '$filepath'",
                              status => 404,
                             );
                return;
        }

        my $filename = $reportfile_result->filename;
        my ($FH, $tmpname) = tempfile (TEMPLATE => "tapper-api-reportfile-${file_id}-XXXX",
                                       UNLINK => 0,
                                       TMPDIR => 1);
        binmode $FH;
        print $FH $reportfile_result->filecontent;
        $self->render_file(filepath => $tmpname, filename => $filename);
        unlink $FH;

        # my $contenttype = $reportfile_result->contenttype eq 'plain' ? 'text/plain' : $reportfile_result->contenttype;
        # my $disposition = $contenttype =~ /plain/ ? 'inline' : 'attachment';

        # $self->res->headers->content_type ($contenttype || 'application/octet-stream');
        # $self->res->headers->content_disposition("$disposition; filename=$filename;");
        # $self->render(text => $reportfile_result->filecontent, status => 202);
};


put 'host-delete/id/:host' => sub {
        my $self = shift;
        my $host = $self->param('host');
        $self->render(json => {$host => {success => 'deleted'}}, status => 202,);
};

1;

=pod

=encoding UTF-8

=head1 NAME

Tapper::API::Plugin::Integrationtest - API functions for integration tests

=head1 AUTHOR

Tapper Team <tapper-ops@amazon.com>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2021 by Amazon.

This is free software, licensed under:

  The (two-clause) FreeBSD License

=cut

__DATA__
@@querytestplanfilelistidtestplan_id.html.ep
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
 <head>
  <title>testplan file list</title>
 </head>
 <body>
<table>
% foreach my $file_id (@$file_ids){
    <tr><td valign="top"><a href="<%= $api_base %>/query/reportfile/id/<%= $file_id %>"></a></td>
% }
</table>
</body></html>

@@querytestrunfilelistidtestrun_id.html.ep
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
 <head>
  <title>testrun file list</title>
 </head>
 <body>
<table>
% foreach my $report_id (@$report_ids){
    <tr><td valign="top"><a href="<%= $api_base %>/query/report-filelist/id/<%= $report_id %>"></a></td>
% }
</table>
</body></html>

@@queryreportfilelistidreport_id.html.ep
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
 <head>
  <title>report file list</title>
 </head>
 <body>
<table>
% foreach my $file_id (@$file_ids){
    <tr><td valign="top"><a href="<%= $api_base %>/query/reportfile/id/<%= $file_id %>"></a></td>
% }
</table>
</body></html>


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.