The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
package Tapper::CLI::Testrun;
our $AUTHORITY = 'cpan:TAPPER';
$Tapper::CLI::Testrun::VERSION = '5.0.4';

use 5.010;
use warnings;
use strict;
use feature qw/ say /;
use English qw/ -no_match_vars /;

# list limit default value
my $i_limit_default = 50;


sub b_print_single_testrun {

    my ( $or_testrun ) = @_;

    print  "\n";
    printf "%17s: %s\n", 'Id'   , $or_testrun->id;
    printf "%17s: %s\n", 'Topic', $or_testrun->topic_name;

    if ( $or_testrun->shortname ) {
        printf "%17s: %s\n", 'Shortname', $or_testrun->shortname;
    }
    if ( $or_testrun->testrun_scheduling ) {

        require Tapper::Cmd::Testrun;
        my $or_cmd = Tapper::Cmd::Testrun->new();
        my $hr_testrun_details = $or_cmd->status($or_testrun->id);
        printf "%17s: %s\n", 'State', $hr_testrun_details->{status};
        printf "%17s: %s\n", 'Queue', $or_testrun->testrun_scheduling->queue->name;

        if ( $or_testrun->testrun_scheduling->status eq 'schedule' ) {
            if ( $or_testrun->testrun_scheduling->requested_hosts->count ) {
                printf "%17s: %s\n", q#Requested Host's#, join ",", $or_testrun->testrun_scheduling->requested_hosts->related_resultset('host')->get_column('name')->all;
            }
        }
        else {
            if (
                $or_testrun->testrun_scheduling->host &&
                $or_testrun->testrun_scheduling->host->name
            ) {
                printf "%17s: %s\n", 'Used Host', $or_testrun->testrun_scheduling->host->name;
            }
        }

        printf "%17s: %s\n", 'Auto rerun', $or_testrun->testrun_scheduling->auto_rerun ? 'yes' : 'no';

    }
    else {
        printf "%17s: %s\n", q##, 'Old testrun without scheduling information';
    }

    if ( $or_testrun->notes ) {
        printf "%17s: %s\n", 'Notes', $or_testrun->notes;
    }

    printf "%17s: ",  q#Precondition Id's#;
    if ( my @a_preconditions = $or_testrun->ordered_preconditions ) {
        say join ", ", map {$_->id} @a_preconditions;
    }
    else {
        say 'None';
    }

    return 1;

}


sub b_print_testruns {

    my ( $or_testruns ) = @_;

    if ( $or_testruns->isa('DBIx::Class::ResultSet') ) {
        for my $or_testrun ( $or_testruns->all ) {
            b_print_single_testrun( $or_testrun );
        }
    }
    else {
        b_print_single_testrun( $or_testruns );
    }
    print  "\n";

    return 1;

}


sub ar_get_list_testrun_parameters {
    return [
        [ 'id|i=i@'   , 'list particular testruns', 'Can be given multiple times.',                                                     ],
        [ 'finished|f', 'list finished testruns, OR combined with other state filters',                                                 ],
        [ 'running|r' , 'list running testruns, OR combined with other state filters',                                                  ],
        [ 'schedule|s', 'list scheduled testruns, OR combined with other state filters',                                                ],
        [ 'prepare|p' , 'list testruns not yet in any scheduling queue, OR combined with other state filters',                          ],
        [ 'queue|q=s@', 'list testruns assigned to this queue, OR combined with other queues, AND combined with other filters',         ],
        [ 'host|h=s@' , 'list testruns assigned to this queue, OR combined with other hosts, AND combined with other filters',          ],
        [ 'limit|l=i'   , "limit the number of testruns (default = $i_limit_default). A value smaller than 1 deactivates the limit.",   ],
        [ 'verbose|v' , 'print all output, without only print ids',                                                                     ],
        [ 'help|?'    , 'Print this help message and exit.',                                                                            ],
    ];
}


sub ar_get_rerun_testrun_parameters {
    return [
        [ 'id|i=i@'   , 'rerun particular testruns', 'Can be given multiple times.', ],
        [ 'notes|n=s' , 'add a description for new testruns',                        ],
        [ 'verbose|v' , 'print all output, without only print ids',                  ],
        [ 'help|?'    , 'Print this help message and exit.',                         ],
    ];
}


sub ar_get_delete_testrun_parameters {
    return [
        [ 'id|i=i@'  , 'delete particular testruns', 'Can be given multiple times.', ],
        [ 'force|f'  , 'really execute the command',                                 ],
        [ 'verbose|v', 'print all output, without only print ids',                   ],
        [ 'help|?'   , 'Print this help message and exit.',                          ],
    ];
}


sub ar_get_create_testrun_parameters {
    return [
        [
            'macroprecond|m=s',
            'use this macro precondition file',
        ],[
            'precondition|p=i@',
            'assigned precondition ids',
        ],[
            'owner|o=s',
            'default=$USER; user login name',
        ],[
            'topic|t=s',
            'default=Misc; one of: Kernel, Xen, KVM, Hardware, Distribution, Benchmark, Software, Misc',
        ],[
            'queue|q=s',
            'default=AdHoc',
        ],[
            'notes|n=s',
            'notes',
        ],[
            'rerun_on_error=i',
            'retry this testrun this many times if an error occurs',
        ],[
            'shortname|s=s',
            'shortname',
        ],[
            'earliest|e=s',
            q#default=now; don't start testrun before this time (format: YYYY-MM-DD hh:mm:ss or now)#,
        ],[
            'requested_host|rh=s@',
            'String; name one possible host for this testrequest;',
            'multiple requested hosts are OR evaluated, i.e. each is appropriate',
        ],[
            'requested_feature|rf=s@',
            'description of one requested feature of a matching host for this testrequest;',
            'multiple requested features are AND evaluated, i.e. each must fit;',
            'not evaluated if a matching requested host is found already',
        ],[
            'notify:s',
            q#create a notification for when the testrun is finished, possibly with filter for 'fail' or 'success'#,
        ],[
            'wait_after_tests',
            'default=0; wait after testrun for human investigation',
        ],[
            'verbose|v',
            'some more informational output',
        ],[
            'dryrun',
            'default=0; only print the preconditions to stdout and then exit',
        ],[
            'auto_rerun',
            'default=0; put this testrun into db again when it is chosen by scheduler',
        ],[
            'priority',
            'This is a very important testrun that should bypass scheduling and not wait for others',
        ],[
            'D=s%',
            'Define a key=value pair used in macro preconditions',
        ],[
            'help|?',
            'Print this help message and exit',
        ],
    ];
}


sub create_macro_preconditions {

    my ( $hr_options ) = @_;

    my $hr_d = $hr_options->{D}; # options are auto-down-cased
    my $b_dryrun = $hr_options->{dryrun};
    my $s_ttapplied = $hr_options->{macroprecond_evaluated};

    if ($b_dryrun) {
        print $s_ttapplied;
        exit 0;
    }

    require Tapper::Cmd::Precondition;
    return Tapper::Cmd::Precondition->new->add( $s_ttapplied );

}


sub s_create_testrun_parameter_check {

    my ( $hr_options ) = @_;

    if ( !$hr_options->{precondition} && !$hr_options->{macroprecond} ) {
        return q#At least one of "precondition" or "macroprecond" is required#;
    }
    if ( exists $hr_options->{rerun_on_error} && $hr_options->{rerun_on_error} !~ /^\d+$/ ) {
        return "value for rerun_on_error ($hr_options->{rerun_on_error}) is not an integer value";
    }
    if ( $hr_options->{earliest} ) {
        require DateTime::Format::Natural;
        my $or_parser           = DateTime::Format::Natural->new;
        $hr_options->{earliest} = $or_parser->parse_datetime( $hr_options->{earliest} );
        if ( $or_parser->success ) {
            if ( $hr_options->{verbose} ) {
                say $hr_options->{earliest}->strftime('%d.%m.%Y %T');
            }
        }
        else {
            return $or_parser->error;
        }
    }

    require Tapper::Model;
    if ( $hr_options->{requested_host} ) {
        for my $i_counter ( 0..$#{$hr_options->{requested_host}} ) {
            if (
                my $or_host =
                    Tapper::Model::model('TestrunDB')
                        ->resultset('Host')
                        ->search(
                            { name => $hr_options->{requested_host}[$i_counter] },
                            { rows => 1 },
                        )
                        ->first
            ) {
                $hr_options->{requested_host}[$i_counter] = $or_host->id;
            }
            else {
                die "Host '$hr_options->{requested_host}[$i_counter]' does not exist\n";
            }
        }
    }
    if ( $hr_options->{notify} ) {
        $hr_options->{notify} = lc $hr_options->{notify};
        if (! grep { $hr_options->{notify} eq $_ } qw/ pass fail / ) {
            die "invalid value for 'notify': valid values 'pass', 'fail'\n";
        }
    }

    require File::Slurp;
    if ( $hr_options->{macroprecond} ) {
        if ( -e $hr_options->{macroprecond} ) {

            require Tapper::Cmd;
            my $or_cmd = Tapper::Cmd->new;
            my $s_mpc_file = $hr_options->{macroprecond};
            my $hr_d = $hr_options->{D};

            $hr_options->{macroprecond_evaluated} = $or_cmd->apply_macro($s_mpc_file, $hr_d);
            if ( (my $s_required) = $hr_options->{macroprecond_evaluated} =~/^# (?:tapper[_-])?mandatory[_-]fields:\s*(.+)/m ) {
                my $re_delim = qr/,+\s*/;
                foreach my $s_field ( split $re_delim, $s_required ) {
                    $s_field =~ s/\s+//g;
                    my ( $s_name, undef ) = split /\./, $s_field;
                    if (! $hr_options->{D}{$s_name} ) {
                        die "Expected macro field '$s_name' missing.\n";
                    }
                }
            }

        }

    }

    return;

}


sub b_create_testrun {

    my ( $or_app_rad ) = @_;

    my $ar_parameters = ar_get_create_testrun_parameters();
    $or_app_rad->getopt( map { $_->[0] } @{$ar_parameters} );
    my $hr_options = $or_app_rad->options;

    if ( $hr_options->{help} ) {
        say {*STDERR} "Usage: $PROGRAM_NAME testrun-new [options]";
        require Tapper::CLI::Base;
        Tapper::CLI::Base::b_print_help( $ar_parameters );
        return;
    }

    if ( my $s_error = s_create_testrun_parameter_check( $hr_options ) ) {
        die 'error: ' . $s_error . "\n";
    }

    my @a_ids;
    if ( $hr_options->{macroprecond} ) {
        @a_ids = create_macro_preconditions( $hr_options );
    }
    if ( $hr_options->{precondition} ) {
        push @a_ids, @{$hr_options->{precondition}};
    }

    if (! @a_ids ) {
        die "error: No valid preconditions given\n";
    }

    require DateTime;
    my $hr_testrun = {
        wait_after_tests    => $hr_options->{wait_after_tests},
        priority            => $hr_options->{priority}            || 0,
        auto_rerun          => $hr_options->{auto_rerun}          || 0,
        earliest            => $hr_options->{earliest}            || DateTime->now,
        notes               => $hr_options->{notes}               || q##,
        owner               => $hr_options->{owner}               || $ENV{USER},
        queue               => $hr_options->{queue}               || 'AdHoc',
        rerun_on_error      => $hr_options->{rerun_on_error} ? int( $hr_options->{rerun_on_error} ) || 0 : 0,
        shortname           => $hr_options->{shortname}           || q##,
        topic               => $hr_options->{topic}               || 'Misc',
    };

    if ( exists $hr_options->{notify} ) {
        $hr_testrun->{notify} = $hr_options->{notify};
    }

    require Tapper::Cmd::Testrun;
    my $or_cmd = Tapper::Cmd::Testrun->new();
    my ( $i_testrun_id, $s_error ) = $or_cmd->add( $hr_testrun );

    if ( $i_testrun_id ) {
        if ( $s_error ) {
            say {*STDERR} "warning: $s_error";
        }
    }
    else {
        if ( $s_error ) {
            die "error: Can't create new testrun\n$s_error\n";
        }
        else {
            die "error: Can't create new testrun because of an unknown error\n";
        }
    }

    require Tapper::Model;
    my $or_testrun_search =
        Tapper::Model::model('TestrunDB')
            ->resultset('Testrun')
            ->find( $i_testrun_id )
    ;

    if ( my $retval = $or_cmd->assign_preconditions( $i_testrun_id, @a_ids ) ) {
        $or_testrun_search->delete();
        die $retval . "\n";
    }

    require Tapper::Cmd::Requested;

    my $or_cmd_req;
    if ( $hr_options->{requested_host} ) {
        $or_cmd_req = Tapper::Cmd::Requested->new;
        foreach my $s_host ( @{$hr_options->{requested_host}} ) {
            if (! $or_cmd_req->add_host( $i_testrun_id, $s_host, ) ) {
                die "error: adding host failed\n";
            }
        }
    }

    if ( $hr_options->{requested_feature} ) {
        $or_cmd_req ||= Tapper::Cmd::Requested->new;
        foreach my $s_feature ( @{$hr_options->{requested_feature}} ) {
            if (! $or_cmd_req->add_feature( $i_testrun_id, $s_feature ) ) {
                die "error: adding feature failed\n";
            }
        }
    }

    require DateTime;
    $or_testrun_search->testrun_scheduling->updated_at( DateTime->now->strftime('%F %T') );
    $or_testrun_search->testrun_scheduling->status('schedule');
    $or_testrun_search->testrun_scheduling->update;

    if ( $hr_options->{verbose} ) {
        say $or_testrun_search->to_string;
    }
    else {
        if ( $ENV{TAPPER_WITH_WEB} ) {
            my $s_webserver = Tapper::Config->subconfig->{webserver};
            say "http://$s_webserver/tapper/testrun/id/$i_testrun_id";
        }
        else {
            say $i_testrun_id;
        }
    }

    return;

}


sub b_delete {

    my ( $or_app_rad ) = @_;

    my $ar_parameters = ar_get_delete_testrun_parameters();
    $or_app_rad->getopt( map { $_->[0] } @{$ar_parameters} );
    my $hr_options = $or_app_rad->options;

    if ( $hr_options->{help} ) {
        say {*STDERR} "Usage: $PROGRAM_NAME testrun-delete [options]";
        require Tapper::CLI::Base;
        Tapper::CLI::Base::b_print_help( $ar_parameters );
        return;
    }

    if (! $hr_options->{force} ) {
        say {*STDERR} "info: Skip all testruns. Use --force.";
        return;
    }

    require Tapper::Cmd::Testrun;
    my $or_cmd = Tapper::Cmd::Testrun->new();
    for my $i_testrun_id ( @{$hr_options->{id}} ){
        if ( my $s_error = $or_cmd->del( $i_testrun_id ) ) {
            die "error: Can not delete testrun $i_testrun_id: $s_error\n";
        }
        if ( $hr_options->{verbose} ) {
            say "info: deleted testrun $i_testrun_id\n";
        }
    }

    return;

}


sub b_rerun {

    my ( $or_app_rad ) = @_;

    my $ar_parameters = ar_get_rerun_testrun_parameters();
    $or_app_rad->getopt( map { $_->[0] } @{$ar_parameters} );
    my $hr_options = $or_app_rad->options;

    if ( $hr_options->{help} ) {
        say {*STDERR} "Usage: $PROGRAM_NAME testrun-rerun [options]";
        require Tapper::CLI::Base;
        Tapper::CLI::Base::b_print_help( $ar_parameters );
        return;
    }

    require Tapper::Cmd::Testrun;
    my $or_testrun = Tapper::Cmd::Testrun->new();

    if ( my $ar_testrun_ids = $hr_options->{id} ) {
        for my $i_testrun_id ( @{$ar_testrun_ids} ) {
            if ( my $i_new_testrun_id = $or_testrun->rerun( $i_testrun_id, $hr_options ) ) {
                if ( $hr_options->{verbose} ) {
                    my $or_new_testrun = Tapper::Model::model->resultset('TestRun')->find($i_new_testrun_id);
                    b_print_testruns( $or_new_testrun );
                    say 'info: original id: ', $i_testrun_id;
                }
                else {
                        say $i_new_testrun_id;
                }
            }
            else {
                die "error: Can't restart testrun $i_testrun_id\n";
            }
        }
        return;
    }
    else {
        die "error: missing required parameter id\n";
    }

}


sub ar_get_queue_ids {

    my ( $or_schema, $ar_queue_names ) = @_;

    my @a_check_queues;
    foreach my $s_queue ( @{$ar_queue_names} ) {
        if (
            my $or_queue_rs =
                $or_schema
                    ->resultset('Queue')
                    ->search({
                        name => $s_queue,
                    },{
                        'select'   => [ 'id' ],
                    })
        ) {
            push @a_check_queues, $or_queue_rs->get_column('id')->all
        }
        else {
            die "error: No such queue: $s_queue\n";
        }
    }

    if ( @a_check_queues ) {
        return @a_check_queues;
    }
    return;

}


sub ar_get_host_ids {

    my ( $or_schema, $ar_host_names ) = @_;

    my @a_check_hosts = ();
    foreach my $s_host ( @{$ar_host_names} ) {
        if (
            my $or_host_rs =
                $or_schema
                    ->resultset('Host')
                    ->search({
                        name => $s_host
                    },{
                        'select'  => [ 'id' ],
                    })
        ) {
            push @a_check_hosts, $or_host_rs->get_column('id')->all;
        }
        else {
            die "error: No such host: $s_host\n";
        }
    }

    if ( @a_check_hosts ) {
        return @a_check_hosts;
    }
    return;

}


sub b_list_testrun {

    my ( $or_app_rad ) = @_;

    my $or_querylog;

    require Tapper::Model;
    my $or_schema = Tapper::Model::model('TestrunDB');

    if ( $ENV{DEBUG} ) {
        require DBIx::Class::QueryLog;
        $or_querylog = DBIx::Class::QueryLog->new();
        $or_schema->storage->debugobj( $or_querylog );
        $or_schema->storage->debug( 1 );
    }

    my $ar_parameters = ar_get_list_testrun_parameters();
    $or_app_rad->getopt( map { $_->[0] } @{$ar_parameters} );
    my %h_options = %{$or_app_rad->options};

    if ( $h_options{help} ) {
        say {*STDERR} "Usage: $PROGRAM_NAME testrun-rerun [options]";
        require Tapper::CLI::Base;
        Tapper::CLI::Base::b_print_help( $ar_parameters );
        return;
    }

    my @a_given_options = grep {
        $h_options{$_}
    } qw/ finished running schedule prepare queue host /;

    my $hr_search      = {};
    my @a_check_queues = ();

    if ( my $i_testrun_id = $h_options{id} ) {

        if ( @a_given_options ) {
            print {*STDERR} "error: other filters doesn't make sense with filter 'id'\n";
        }

        # filter 'id' doesn't make sense without verbose
        $h_options{verbose} = 1;

        $hr_search = { 'me.id' => $i_testrun_id }

    }
    elsif ( @a_given_options ) {

        if ( my @a_state_selection = grep { $h_options{$_} } qw/ finished running schedule prepare / ) {
            $hr_search->{'testrun_scheduling.status'} ||= \@a_state_selection;
        }

        if ( my $ar_queue_ids = ar_get_queue_ids( $or_schema, $h_options{queue} ) ) {
            $hr_search->{'testrun_scheduling.queue_id'} = { -in => $ar_queue_ids };
        }

        if ( my $ar_host_ids = ar_get_queue_ids( $or_schema, $h_options{host} ) ) {
            $hr_search->{-or} = [
                -and => [
                    -not                         => { 'testrun_scheduling.status' => 'schedule',       },
                    'testrun_scheduling.host_id' => { -in                         => $ar_host_ids,  },
                ],
                -and => [
                    'testrun_scheduling.status'  => 'schedule',
                    'requested_hosts.host_id'    => { -in  => $ar_host_ids, },
                ],
            ];
        }

    }

    my $hr_search_options = {
        order_by => { -desc => 'me.id' }
    };

    if ( exists $h_options{limit} ) {
        if ( $h_options{limit} > 0 ) {
            $hr_search_options->{rows} = $h_options{limit};
        }
    }
    else {
        $hr_search_options->{rows} = $i_limit_default;
    }
    if ( $h_options{verbose} ) {
        $hr_search_options->{'prefetch'} = [
            {
                'testrun_scheduling' => [
                    'queue',
                    {
                        'requested_hosts' => 'host',
                    },
                ],
            },
            {
                'testrun_scheduling' => 'host',
            },
        ];
    }
    else {
        $hr_search_options->{'join'}   = 'testrun_scheduling';
        $hr_search_options->{'select'} = ['id'];
    }

    my $or_testrun_rs =
        $or_schema
            ->resultset('Testrun')
            ->search( $hr_search, $hr_search_options )
    ;

    if ( $h_options{verbose} ) {
        Tapper::CLI::Testrun::b_print_testruns( $or_testrun_rs );
    }
    else {
        foreach my $i_testrun_id ( $or_testrun_rs->get_column('id')->all ) {
            say $i_testrun_id;
        }
    }

    if ( $ENV{DEBUG} ) {

        require DBIx::Class::QueryLog::Analyzer;
        my $or_analyzer = DBIx::Class::QueryLog::Analyzer->new({
            querylog => $or_querylog,
        });

        require Data::Dumper;
        say {*STDERR} "Query count: " . scalar( @{$or_analyzer->get_sorted_queries} );
        say {*STDERR} Data::Dumper::Dumper([
            $or_analyzer->get_sorted_queries
        ]);

    }

    return;

}


sub testrun_update
{
        my ($c) = @_;
        $c->getopt( 'id=i@','status=s', 'topic=s', 'auto-rerun!','help|?', 'verbose|v' );
        if ( $c->options->{help} or not $c->options->{id}) {
                say STDERR "Please set at least one testrun id with --id!" unless @{$c->options->{id} || []};
                say STDERR "Please set an update action" unless ($c->options->{state} or defined $c->options->{"auto-rerun"});
                say STDERR "$PROGRAM_NAME testrun-update --id=s@ --status=s --auto_rerun --no-auto-rerun --verbose|v [--help|?]";
                say STDERR "    --id            Id of the testrun to update, can be given multiple times";
                say STDERR "    --topic         one of: Kernel, Xen, KVM, Hardware, Distribution, Benchmark, Software, Misc";
                say STDERR "    --status        Set testrun to given status, can be one of 'prepare', 'schedule', 'finished'.";
                say STDERR "    --auto-rerun    Activate auto-rerun on testrun. ";
                say STDERR "    --no-auto-rerun Activate auto-rerun on testrun";
                say STDERR "    --verbose|v     Print new state of testrun (will only print id of updated testruns without)";
                say STDERR "    --help|?        Print this help message and exit";
                return;
        }

        require Tapper::Model;
 ID:
        foreach my $testrun_id (@{$c->options->{id}}) {
                my $testrun = Tapper::Model::model('TestrunDB')->resultset('Testrun')->find($testrun_id);
                if (not $testrun) {
                        say STDERR "Testrun with id $testrun_id not found. Skipping!";
                        next ID;
                }

                if ( $c->options->{topic} ) {
                    $testrun->update_content({
                        topic => $c->options->{topic},
                    });
                }

                if (not ($testrun->testrun_scheduling->status eq 'prepare' or
                         $testrun->testrun_scheduling->status eq 'schedule')
                   )
                {
                        say STDERR "Can only update testruns in state 'schedule' and 'finished'. Updating testruns in other states will break something. Please consider tapper testrun-rerun";
                        next ID;
                }

                if ($c->options->{status}) {
                        $testrun->testrun_scheduling->status($c->options->{status});
                        $testrun->testrun_scheduling->update;
                }
                if (defined($c->options->{"auto-rerun"})) {
                        $testrun->testrun_scheduling->auto_rerun($c->options->{"auto-rerun"});
                        $testrun->testrun_scheduling->update;
                }
                if ($c->options->{verbose}) {
                        b_print_testruns($testrun);
                } else {
                        say $testrun_id;
                }
        }

}


sub b_cancel
{

        my ($c) = @_;
        $c->getopt( 'id=i@','comment=s','help|?', 'verbose|v' );
        if ( $c->options->{help} or not $c->options->{id}) {
                say STDERR "Please set at least one testrun id with --id!" unless @{$c->options->{id} || []};
                say STDERR "$PROGRAM_NAME testrun-cancel --id=i@ [--comment=s]  [--verbose|v] [--help|?]";
                say STDERR "    --id            Id of the testrun to cancel, can be given multiple times";
                say STDERR "    --comment       A comment why the testrun(s) were cancelled";
                say STDERR "    --verbose|v     Tell user what we just did (without -v only the testrun id will be printed in the success case)";
                say STDERR "    --help|?        Print this help message and exit";
                return;
        }

        require Tapper::Cmd::Testrun;
        my $cmd = Tapper::Cmd::Testrun->new();
        foreach my $id (@{$c->options->{id}}) {
                my $retval = $cmd->cancel($id, $c->options->{comment});
                warn $retval if $retval;
        }
        return;
}



sub setup {

    my ( $or_apprad ) = @_;

    $or_apprad->register( 'testrun-list'   , \&b_list_testrun   , 'Show all testruns matching a given condition', );
    $or_apprad->register( 'testrun-update' , \&testrun_update   , 'Update an existing testrun', );
    $or_apprad->register( 'testrun-rerun'  , \&b_rerun          , 'Rerun an existing testrun with the same preconditions', );
    $or_apprad->register( 'testrun-delete' , \&b_delete         , 'Delete a testrun', );
    $or_apprad->register( 'testrun-cancel' , \&b_cancel         , 'Cancel a running testrun', );
    $or_apprad->register( 'testrun-new'    , \&b_create_testrun , 'Create a testrun', );

    if ( $or_apprad->can('group_commands') ) {
        $or_apprad->group_commands(
            'Testrun commands',
                'testrun-list',
                'testrun-new',
                'testrun-update',
                'testrun-rerun',
                'testrun-delete',
                'testrun-cancel',
        );
    }

    return;

}

1; # End of Tapper::CLI

__END__

=pod

=encoding UTF-8

=head1 NAME

Tapper::CLI::Testrun

=head1 SYNOPSIS

This module is part of the Tapper::CLI framework. It is supposed to be
used together with App::Rad. All following functions expect their
arguments as $c->options->{$arg} unless otherwise stated.

    use App::Rad;
    use Tapper::CLI::Testrun;
    Tapper::CLI::Testrun::setup($c);
    App::Rad->run();

=head1 NAME

Tapper::CLI::Testrun - Tapper - testrun related commands for the tapper CLI

=head1 FUNCTIONS

=head2 b_print_single_testrun

print column data for a single testrun row to STDOUT

=head2 b_print_testruns

print column data for resultset to STDOUT

=head2 ar_get_list_testrun_parameters

return list testrun parameters and descriptions

=head2 ar_get_rerun_testrun_parameters

return rerun testrun parameters and descriptions

=head2 ar_get_delete_testrun_parameters

return delete testrun parameters and descriptions

=head2 ar_get_create_testrun_parameters

return create testrun parameters and descriptions

=head2 create_macro_preconditions

Process a macroprecondition. This includes substitions using
Template::Toolkit, separating the individual preconditions that are part of
the macroprecondition and putting them into the database. Parameters fit the
App::Cmd::Command API.

@param hashref - hash containing options
@param hashref - hash containing arguments

@returnlist array containing precondition ids

=head2 s_create_testrun_parameter_check

check command line parameters for create testrun and return error if exists

=head2 b_create_testrun

create a testrun

=head2 b_delete

delete a testrun

=head2 b_rerun

rerun an existing testrun

=head2 ar_get_queue_ids

return an array reference of queue_ids for a an array reference of queue_names

=head2 ar_get_host_ids

return an array reference of host_ids for a an array reference of host_names

=head2 b_list_testrun

list existing restuns

=head2 testrun_update

Update values of an existing testrun.

=head2 b_cancel

Cancel a running testrun. If the given testrun is currently not running,
the function does the obvious right thing and also warns the user.

=head2 setup

Initialize the testplan functions for tapper CLI

=head1 AUTHOR

AMD OSRC Tapper Team <tapper@amd64.org>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2016 by Advanced Micro Devices, Inc..

This is free software, licensed under:

  The (two-clause) FreeBSD License

=cut