Ver Fonte

Implement most of #64, still needs tests.

Do tests possibly as part of 66
George S. Baugh há 10 anos atrás
pai
commit
852c2528b3

+ 4 - 0
Changes

@@ -3,7 +3,11 @@ Revision history for Perl module TestRail::API
 0.029 2015-07-11 TEODESIAN
     - Add bulkAddResults function to TestRail::API
     - Add new script testrail-bulk-mark-results
+    - Add new script testrail-lock
     - Re-factor part of the scripts into TestRail::Utils::interrogateUser
+    - Modify all bin/ scripts to use POD as their help output, move help() to TestRail::Utils
+    - Modify all bin/ scripts to parse all the ~/.testrailrc options
+    - Fix an issue where statusNamesToIDs would return status IDs in the wrong order.
 
 0.028 2015-06-16 TEODESIAN
     - Hotfix: forgot to include a module in the prove plugin.  How did this pass compile.t? A mystery.

+ 77 - 70
bin/testrail-bulk-mark-results

@@ -12,44 +12,61 @@ Sometimes it is useful to mark entire runs of tests when, for example, a prerequ
 For example, if a binary produced for test fails to run at all, more detailed testing will be impossible;
 it would save time to just mark everything as blocked.
 
-=head2 PARAMETERS:
+=head1 PARAMETERS:
 
-=head3 MANDATORY PARAMETERS
+=head2 MANDATORY PARAMETERS
 
-    -j --project [project]: desired project name.
-    -r --run [run]: desired run name.
+=over 4
 
-=head3 SEMI-OPTIONAL PARAMETERS
+--apiurl     : full URL to get to TestRail index document
 
-    -p --plan [plan]: desired plan name.  Required if the run passed is a child of a plan.
-    -e --encoding: Character encoding of arguments.  Defaults to UTF-8.
-                   See L<Encode::Supported> for supported encodings.
+--password   : Your TestRail Password, or a valid API key (TestRail 4.2 and above).
 
-=head3 OPTIONAL PARAMETERS
+--user       : Your TestRail User Name.
 
-    -c --config [config]: configuration name to filter plans in run.  Can be passed multiple times.
-    -a --assignedto [user]: only mark tests assigned to user. Can be passed multiple times.
+-j --project : desired project name.
 
-=head3 CONFIG OPTIONS
+-r --run     : desired run name.
 
-    In your \$HOME, (or the current directory, if your system has no
-    concept of a home directory) put a file called .testrailrc with
-    key=value syntax separated by newlines.
-    Valid Keys are: apiurl,user,password
+=back
 
-=head3 CONFIG OVERRIDES
+All mandatory options not passed with the above switches, or in your ~/.testrailrc will be prompted for.
 
-    These override the config, if present.
-    If neither are used, you will be prompted.
+=head2 SEMI-OPTIONAL PARAMETERS
 
-  --apiurl   [url] : full URL to get to TestRail index document
-  --password [key] : Your TestRail Password, or a valid API key (TestRail 4.2 and above).
-  --user    [name] : Your TestRail User Name.
+=over 4
 
-=head2 TESTING OPTIONS:
+-p --plan     : desired plan name.  Required if the run passed is a child of a plan.
 
-    --mock: don't do any real HTTP requests.
-    --help: show this output
+-e --encoding : Character encoding of arguments.  Defaults to UTF-8. See L<Encode::Supported> for supported encodings.
+
+=back
+
+=head2 OPTIONAL PARAMETERS
+
+=over 4
+
+-c --config     : configuration name to filter plans in run.  Can be passed multiple times.
+
+-a --assignedto : only mark tests assigned to user. Can be passed multiple times.
+
+=back
+
+=head1 CONFIGURATION FILE
+
+In your \$HOME, (or the current directory, if your system has no concept of a home directory) put a file called .testrailrc with key=value syntax separated by newlines.
+Valid Keys are the same as documented by L<App::Prove::Plugin::TestRail>.
+All options specified thereby are overridden by passing the command-line switches above.
+
+=head1 MISCELLANEOUS OPTIONS:
+
+=over 4
+
+--mock : don't do any real HTTP requests.
+
+--help : show this output
+
+=back
 
 =cut
 
@@ -64,86 +81,76 @@ use Getopt::Long;
 Getopt::Long::Configure('pass_through');
 
 use File::HomeDir qw{my_home};
-use File::Find;
-use Cwd qw{abs_path};
-use File::Basename qw{basename};
 
-use Pod::Perldoc 3.10;
+my $opts = {};
 
-sub help {
-    @ARGV = ($0);
-    Pod::Perldoc->run();
-    exit 0;
+# Parse config file
+my $homedir = my_home() || '.';
+if (-e $homedir . '/.testrailrc') {
+    $opts = TestRail::Utils::parseConfig($homedir);
 }
 
-my %opts;
-
+# Override configuration with switches
 GetOptions(
-    'apiurl=s'        => \$opts{'apiurl'},
-    'password=s'      => \$opts{'pass'},
-    'user=s'          => \$opts{'user'},
-    'j|project=s'     => \$opts{'project'},
-    'p|plan=s'        => \$opts{'plan'},
-    'r|run=s'         => \$opts{'run'},
-    'c|config=s@'     => \$opts{'configs'},
-    'a|assignedto=s@' => \$opts{'users'},
-    'mock'            => \$opts{'mock'},
-    'e|encoding=s'    => \$opts{'encoding'},
-    'h|help'          => \$opts{'help'}
+    'apiurl=s'        => \$opts->{'apiurl'},
+    'password=s'      => \$opts->{'password'},
+    'user=s'          => \$opts->{'user'},
+    'j|project=s'     => \$opts->{'project'},
+    'p|plan=s'        => \$opts->{'plan'},
+    'r|run=s'         => \$opts->{'run'},
+    'c|config=s@'     => \$opts->{'configs'},
+    'a|assignedto=s@' => \$opts->{'users'},
+    'mock'            => \$opts->{'mock'},
+    'e|encoding=s'    => \$opts->{'encoding'},
+    'h|help'          => \$opts->{'help'}
 );
 
-if ($opts{help}) { help(); }
+if ($opts->{help}) { TestRail::Utils::help(); }
 
 my $status = $ARGV[0];
 my $reason = $ARGV[1];
 
 die("No status to set provided.") unless $status;
-#Parse config file if we are missing api url/key or user
-my $homedir = my_home() || '.';
-if (-e $homedir . '/.testrailrc' && (!$opts{apiurl} || !$opts{pass} || !$opts{user}) ) {
-    ($opts{apiurl},$opts{pass},$opts{user}) = TestRail::Utils::parseConfig($homedir,1);
-}
-
-TestRail::Utils::interrogateUser(\%opts,qw{apiurl user pass project run});
+TestRail::Utils::interrogateUser($opts,qw{apiurl user password project run});
 
-if ($opts{mock}) {
+if ($opts->{mock}) {
     use Test::LWP::UserAgent::TestRailMock;
-    $opts{browser} = $Test::LWP::UserAgent::TestRailMock::mockObject;
-    $opts{debug} = 1;
+    $opts->{browser} = $Test::LWP::UserAgent::TestRailMock::mockObject;
+    $opts->{debug} = 1;
 }
 
-my $tr = TestRail::API->new($opts{apiurl},$opts{user},$opts{pass},$opts{'encoding'},$opts{'debug'});
-$tr->{'browser'} = $opts{'browser'} if $opts{'browser'};
+my $tr = TestRail::API->new($opts->{apiurl},$opts->{user},$opts->{password},$opts->{'encoding'},$opts->{'debug'});
+$tr->{'browser'} = $opts->{'browser'} if $opts->{'browser'};
 $tr->{'debug'} = 0;
 
-my $project = $tr->getProjectByName($opts{'project'});
+my $project = $tr->getProjectByName($opts->{'project'});
 if (!$project) {
-    print "No such project '$opts{project}'.\n";
+    print "No such project '$opts->{project}'.\n";
     exit 6;
 }
 
 my ($run,$plan);
 
-if ($opts{'plan'}) {
-    $plan = $tr->getPlanByName($project->{'id'},$opts{'plan'});
+if ($opts->{'plan'}) {
+    $plan = $tr->getPlanByName($project->{'id'},$opts->{'plan'});
     if (!$plan) {
-        print "No such plan '$opts{plan}'!\n";
+        print "No such plan '$opts->{plan}'!\n";
         exit 1;
     }
-    $run = $tr->getChildRunByName($plan,$opts{'run'}, $opts{'configs'});
+    $run = $tr->getChildRunByName($plan,$opts->{'run'}, $opts->{'configs'});
 } else {
-    $run = $tr->getRunByName($project->{'id'},$opts{'run'});
+    $run = $tr->getRunByName($project->{'id'},$opts->{'run'});
 }
 
 if (!$run) {
-    print "No such run '$opts{run}' matching the provided configs (if any).\n";
+    print "No such run '$opts->{run}' matching the provided configs (if any).\n";
     exit 2;
 }
 
 my $user_ids;
 #Process assignedto ids
-if ($opts{'users'}) {
-    eval { @$user_ids = $tr->userNamesToIds(@{$opts{'users'}}); };
+if ($opts->{'users'}) {
+    eval { @$user_ids = $tr->userNamesToIds(@{$opts->{'users'}}); };
     if ($@) {
         print "$@\n";
         exit 5;
@@ -164,7 +171,7 @@ my ($status_id) = $tr->statusNamesToIds($status);
         'test_id' => $_->{'id'},
         'status_id' => $status_id,
         'comment'   => $reason,
-        'version'   => $opts{'version'}
+        'version'   => $opts->{'version'}
     }
 } @$cases;
 

+ 223 - 0
bin/testrail-lock

@@ -0,0 +1,223 @@
+#!/usr/bin/env perl
+# ABSTRACT: Lock a test in a TestRail, and return the test name if successful.
+# PODNAME: testrail-lock
+
+=head1 SYNOPSIS
+
+  # Lock a group of tests and execute them
+  testrail-tests [OPTIONS] | xargs testrail-lock [OPTIONS] | xargs prove -PTestrail=...
+
+=head1 DESCRIPTION
+
+testrail-lock - pick an untested/retest test in TestRail, lock it, and return the test name if successful.
+
+It is useful to lock the test in situations where you have multiple disconnected test running processes trying to allocate resources toward testing outstanding cases so that effort is not duplicated.
+This is accomplished via setting a special locking result on a test rather than simple assignment, as detecting lock conflicts is impossible then due to a lack of assignment history.
+Results, however have a history of results set, so we use that fact to detect if a locking collision occured (race condition) and fail to return a result when another process locked during our attempt to lock.
+
+Will respect test priority when making the choice of what test to lock.
+
+This obviously does not make sense with case_per_ok test upload; support for locking entire sections when in case_per_ok upload mode is not supported at this time.
+
+=head1 PARAMETERS:
+
+=head2 MANDATORY PARAMETERS
+
+=over 4
+
+--apiurl      : full URL to get to TestRail index document
+
+--password    : Your TestRail Password, or a valid API key (TestRail 4.2 and above).
+
+--user        : Your TestRail User Name.
+
+-j --project  : desired project name.
+
+-r --run      : desired run name.
+
+-l --lockname : internal name of lock status.
+
+=back
+
+All mandatory options not passed with the above switches, or in your ~/.testrailrc will be prompted for.
+
+=head2 SEMI-OPTIONAL PARAMETERS
+
+=over 4
+
+-p --plan     : desired plan name.  Required if the run passed is a child of a plan.
+
+-e --encoding : Character encoding of arguments.  Defaults to UTF-8. See L<Encode::Supported> for supported encodings.
+
+=back
+
+=head2 OPTIONAL PARAMETERS
+
+=over 4
+
+-c --config : configuration name to filter plans in run.  Can be passed multiple times.
+
+=back
+
+=head1 CONFIGURATION FILE
+
+In your $HOME, (or the current directory, if your system has no concept of a home directory) put a file called .testrailrc with key=value syntax separated by newlines.
+Valid Keys are the same as documented by L<App::Prove::Plugin::TestRail>.
+All options specified thereby are overridden by passing the command-line switches above.
+
+=head1 MISCELLANEOUS OPTIONS:
+
+=over 4
+
+--mock : don't do any real HTTP requests.  Used only by tests.
+
+--help : show this output
+
+=back
+
+=cut
+
+use strict;
+use warnings;
+use utf8;
+
+use TestRail::API;
+use TestRail::Utils;
+
+use Getopt::Long;
+use File::HomeDir qw{my_home};
+use File::Find;
+use Cwd qw{abs_path};
+use File::Basename qw{basename};
+use Sys::Hostname qw{hostname};
+
+my $hostname = hostname();
+
+my $opts = {};
+
+#Parse config file if we are missing api url/key or user
+my $homedir = my_home() || '.';
+if (-e $homedir . '/.testrailrc') {
+    $opts = TestRail::Utils::parseConfig($homedir);
+}
+
+GetOptions(
+    'apiurl=s'        => \$opts->{'apiurl'},
+    'password=s'      => \$opts->{'password'},
+    'user=s'          => \$opts->{'user'},
+    'l|lockname=s'    => \$opts->{'lockname'},
+    'j|project=s'     => \$opts->{'project'},
+    'p|plan=s'        => \$opts->{'plan'},
+    'r|run=s'         => \$opts->{'run'},
+    'c|config=s@'     => \$opts->{'configs'},
+    'mock'            => \$opts->{'mock'},
+    'e|encoding=s'    => \$opts->{'encoding'},
+    'h|help'          => \$opts->{'help'}
+);
+
+if ($opts->{help}) { help(); }
+
+TestRail::Utils::interrogateUser($opts,qw{apiurl user password project run lockname});
+
+if ($opts->{mock}) {
+    use Test::LWP::UserAgent::TestRailMock;
+    $opts->{browser} = $Test::LWP::UserAgent::TestRailMock::mockObject;
+    $opts->{debug} = 1;
+}
+
+my $tr = TestRail::API->new($opts->{apiurl},$opts->{user},$opts->{password},$opts->{'encoding'},$opts->{'debug'});
+$tr->{'browser'} = $opts->{'browser'} if $opts->{'browser'};
+$tr->{'debug'} = 0;
+
+my $project = $tr->getProjectByName($opts->{'project'});
+if (!$project) {
+    warn "No such project '$opts->{project}'.\n";
+    exit 6;
+}
+
+my ($run,$plan);
+
+if ($opts->{'plan'}) {
+    $plan = $tr->getPlanByName($project->{'id'},$opts->{'plan'});
+    if (!$plan) {
+        warn "No such plan '$opts->{plan}'!\n";
+        exit 1;
+    }
+    $run = $tr->getChildRunByName($plan,$opts->{'run'}, $opts->{'configs'});
+} else {
+    $run = $tr->getRunByName($project->{'id'},$opts->{'run'});
+}
+
+if (!$run) {
+    warn "No such run '$opts->{run}' matching the provided configs (if any).\n";
+    exit 2;
+}
+
+my $status_ids;
+
+# Process statuses
+@$status_ids = $tr->statusNamesToIds($opts->{'lockname'},'untested','retest');
+my ($lock_status_id,$untested_id,$retest_id) = @$status_ids;
+
+my $cases = $tr->getTests($run->{'id'});
+my @statuses_to_check_for = ($untested_id,$retest_id);
+@statuses_to_check_for = ($lock_status_id) if $opts->{'simulate_race_condition'}; #Unit test stuff
+
+# Limit to only non-locked and open cases
+@$cases = grep { my $tstatus = $_->{'status_id'}; scalar(grep { $tstatus eq $_ } @statuses_to_check_for) } @$cases;
+@$cases = sort { $a->{'priority_id'} <=> $b->{'priority_id'} } @$cases; #Sort by priority
+
+TRY_AGAIN:
+
+my $test = shift @$cases;
+
+if (!$test) {
+    warn "No outstanding cases in the provided run.\n";
+    exit 3;
+}
+
+my $res = $tr->createTestResults($test->{'id'},$lock_status_id,"Test Locked by $hostname.\n\nIf this result is preceded immediately by another lock statement like this, please disregard, as a lock collision occurred.");
+
+#If we've got more than 100 lock conflicts, we have big-time problems
+my $results = $tr->getTestResults($test->{'id'},100);
+
+#Remember, we're returned results from newest to oldest...
+my $next_one = 0;
+foreach my $result (@$results) {
+    unless ($result->{'status_id'} == $lock_status_id) {
+        #Clearly no lock conflict going on here if next_one is true
+        last if $next_one;
+        #Otherwise just skip it until we get to the test we locked
+        next;
+    }
+
+    if ($result->{id} == $res->{'id'}) {
+        $next_one = 1;
+        next;
+    }
+
+    if ($next_one) {
+        #If we got this far, a lock conflict occurred. Try the next one.
+        warn "Lock conflict detected.  Trying again...\n";
+        goto TRY_AGAIN;
+    }
+}
+
+if (!$next_one) {
+    warn "Failed to lock case!";
+    exit 4;
+}
+
+print $test->{'title'}."\n";
+
+exit 0;
+
+__END__
+
+L<TestRail::API>
+
+L<File::HomeDir> for the finding of .testrailrc
+
+=head1 SPECIAL THANKS
+
+Thanks to cPanel Inc, for graciously funding the creation of this distribution.

+ 65 - 64
bin/testrail-runs

@@ -11,39 +11,51 @@
 testrail-tests - list runs in a TestRail project matching the provided filters.
 Groups by plan for runs which are children of a plan.
 
-=head2 PARAMETERS:
+=head1 PARAMETERS:
 
-=head3 MANDATORY PARAMETERS
+=head2 MANDATORY PARAMETERS
 
-    -j --project [project]: desired project name.
+=over 4
 
-=head3 OPTIONAL PARAMETERS
+--apiurl     : full URL to get to TestRail index document
 
-    -c --config [config]: configuration name to filter runs.  Can be passed multiple times.
-    -s --status [status]: only list runs with one or more tests having [status] in testrail.  Can be passed multiple times.
-    -e --encoding: Character encoding of arguments.  Defaults to UTF-8.
-                   See L<Encode::Supported> for supported encodings.
+--password   : Your TestRail Password, or a valid API key (TestRail 4.2 and above).
 
-=head3 CONFIG OPTIONS
+--user       : Your TestRail User Name.
 
-    In your \$HOME, (or the current directory, if your system has no
-    concept of a home directory) put a file called .testrailrc with
-    key=value syntax separated by newlines.
-    Valid Keys are: apiurl,user,password
+-j --project : desired project name.
 
-=head3 CONFIG OVERRIDES
+=back
 
-    These override the config, if present.
-    If neither are used, you will be prompted.
+All mandatory options not passed with the above switches, or in your ~/.testrailrc will be prompted for.
 
-  --apiurl   [url] : full URL to get to TestRail index document
-  --password [key] : Your TestRail Password, or a valid API key (TestRail 4.2 and above).
-  --user    [name] : Your TestRail User Name.
+=head2 OPTIONAL PARAMETERS
 
-=head2 TESTING OPTIONS:
+=over 4
 
-    --mock: don't do any real HTTP requests.
-    --help: show this output
+-c --config   : configuration name to filter runs.  Can be passed multiple times.
+
+-s --status   : only list runs with one or more tests having [status] in testrail.  Can be passed multiple times.
+
+-e --encoding : Character encoding of arguments.  Defaults to UTF-8. See L<Encode::Supported> for supported encodings.
+
+=back
+
+=head1 CONFIGURATION FILE
+
+In your \$HOME, (or the current directory, if your system has no concept of a home directory) put a file called .testrailrc with key=value syntax separated by newlines.
+Valid Keys are the same as documented by L<App::Prove::Plugin::TestRail>.
+All options specified thereby are overridden by passing the command-line switches above.
+
+=head1 MISCELLANEOUS OPTIONS:
+
+=over 4
+
+--mock : don't do any real HTTP requests.
+
+--help : show this output
+
+=back
 
 =cut
 
@@ -56,83 +68,72 @@ use TestRail::Utils;
 
 use Getopt::Long;
 use File::HomeDir qw{my_home};
-use File::Find;
-use Cwd qw{abs_path};
-use File::Basename qw{basename};
 
-use Pod::Perldoc 3.10;
+my $opts ={};
 
-sub help {
-    @ARGV = ($0);
-    Pod::Perldoc->run();
-    exit 0;
+# Parse config file
+my $homedir = my_home() || '.';
+if (-e $homedir . '/.testrailrc') {
+    $opts = TestRail::Utils::parseConfig($homedir);
 }
 
-my %opts;
-
 GetOptions(
-    'apiurl=s'     => \$opts{'apiurl'},
-    'password=s'   => \$opts{'pass'},
-    'user=s'       => \$opts{'user'},
-    'j|project=s'  => \$opts{'project'},
-    'c|config=s@'  => \$opts{'configs'},
-    's|status=s@'  => \$opts{'statuses'},
-    'mock'         => \$opts{'mock'},
-    'e|encoding=s' => \$opts{'encoding'},
-    'h|help'       => \$opts{'help'}
+    'apiurl=s'     => \$opts->{'apiurl'},
+    'password=s'   => \$opts->{'password'},
+    'user=s'       => \$opts->{'user'},
+    'j|project=s'  => \$opts->{'project'},
+    'c|config=s@'  => \$opts->{'configs'},
+    's|status=s@'  => \$opts->{'statuses'},
+    'mock'         => \$opts->{'mock'},
+    'e|encoding=s' => \$opts->{'encoding'},
+    'h|help'       => \$opts->{'help'}
 );
 
-if ($opts{help}) { help(); }
-
-#Parse config file if we are missing api url/key or user
-my $homedir = my_home() || '.';
-if (-e $homedir . '/.testrailrc' && (!$opts{apiurl} || !$opts{pass} || !$opts{user}) ) {
-    ($opts{apiurl},$opts{pass},$opts{user}) = TestRail::Utils::parseConfig($homedir,1);
-}
+if ($opts->{help}) { TestRail::Utils::help(); }
 
-TestRail::Utils::interrogateUser(\%opts,qw{apiurl user pass project});
+TestRail::Utils::interrogateUser($opts,qw{apiurl user password project});
 
-if ($opts{mock}) {
+if ($opts->{mock}) {
     use Test::LWP::UserAgent::TestRailMock;
-    $opts{browser} = $Test::LWP::UserAgent::TestRailMock::mockObject;
-    $opts{debug} = 1;
+    $opts->{browser} = $Test::LWP::UserAgent::TestRailMock::mockObject;
+    $opts->{debug} = 1;
 }
 
-my $tr = TestRail::API->new($opts{apiurl},$opts{user},$opts{pass},$opts{'encoding'},$opts{'debug'});
-$tr->{'browser'} = $opts{'browser'} if $opts{'browser'};
+my $tr = TestRail::API->new($opts->{apiurl},$opts->{user},$opts->{password},$opts->{'encoding'},$opts->{'debug'});
+$tr->{'browser'} = $opts->{'browser'} if $opts->{'browser'};
 $tr->{'debug'} = 0;
 
 my ($status_ids,$user_ids);
 
 #Process statuses
-if ($opts{'statuses'}) {
-    eval { @$status_ids = $tr->statusNamesToIds(@{$opts{'statuses'}}); };
+if ($opts->{'statuses'}) {
+    eval { @$status_ids = $tr->statusNamesToIds(@{$opts->{'statuses'}}); };
     if ($@) {
         print "$@\n";
         exit 4;
     }
 }
 
-my $project = $tr->getProjectByName($opts{'project'});
+my $project = $tr->getProjectByName($opts->{'project'});
 if (!$project) {
-    print "No such project '$opts{project}'.\n";
+    print "No such project '$opts->{project}'.\n";
     exit 6;
 }
 
 my @pconfigs;
-if (defined $opts{configs}) {
+if (defined $opts->{configs}) {
     my $avail_configs = $tr->getConfigurations($project->{'id'});
     my ($cname);
-    @pconfigs = map {$_->{'id'}} grep { $cname = $_->{'name'}; grep {$_ eq $cname} @{$opts{configs}} } @$avail_configs; #Get a list of IDs from the names passed
+    @pconfigs = map {$_->{'id'}} grep { $cname = $_->{'name'}; grep {$_ eq $cname} @{$opts->{configs}} } @$avail_configs; #Get a list of IDs from the names passed
 }
 
-if (defined($opts{configs}) && (scalar(@pconfigs) != scalar(@{$opts{configs}}))) {
+if (defined($opts->{configs}) && (scalar(@pconfigs) != scalar(@{$opts->{configs}}))) {
     print("One or more configurations passed does not exist in your project!\n");
     exit 7;
 }
 
 my ($runs,$plans,$planRuns,$cruns,$found) = ([],[],[],[],0);
-$runs = $tr->getRuns($project->{'id'}) if (!$opts{'configs'}); # If configs are passed, global runs are not in consideration.
+$runs = $tr->getRuns($project->{'id'}) if (!$opts->{'configs'}); # If configs are passed, global runs are not in consideration.
 $plans = $tr->getPlans($project->{'id'});
 foreach my $plan (@$plans) {
     $cruns = $tr->getChildRuns($plan);
@@ -152,10 +153,10 @@ foreach my $plan (@$plans) {
 
 push(@$runs,@$planRuns);
 
-if ($opts{'statuses'}) {
+if ($opts->{'statuses'}) {
     @$runs =  $tr->getRunSummary(@$runs);
     @$runs = grep { defined($_->{'run_status'}) } @$runs; #Filter stuff with no results
-    foreach my $status (@{$opts{'statuses'}}) {
+    foreach my $status (@{$opts->{'statuses'}}) {
         @$runs = grep { $_->{'run_status'}->{$status} } @$runs; #If it's positive, keep it.  Otherwise forget it.
     }
 }

+ 97 - 83
bin/testrail-tests

@@ -10,48 +10,70 @@
 
 testrail-tests - list tests in a run matching the provided filters.
 
-=head2 PARAMETERS:
 
-=head3 MANDATORY PARAMETERS
+=head1 PARAMETERS:
 
-    -j --project [project]: desired project name.
-    -r --run [run]: desired run name.
+=head2 MANDATORY PARAMETERS
 
-=head3 SEMI-OPTIONAL PARAMETERS
+=over 4
 
-    -p --plan [plan]: desired plan name.  Required if the run passed is a child of a plan.
-    -m --match [dir]: attempt to find filenames matching the test names in the provided dir.
-    --no-match [dir]: attempt to find filenames that do not match test names in the provided dir.
-    -n --no-recurse: if match (or no-match) passed, do not recurse subdirectories.
-    -e --encoding: Character encoding of arguments.  Defaults to UTF-8.
-                   See L<Encode::Supported> for supported encodings.
+--apiurl     : full URL to get to TestRail index document
 
-=head3 OPTIONAL PARAMETERS
+--password   : Your TestRail Password, or a valid API key (TestRail 4.2 and above).
 
-    -c --config [config]: configuration name to filter plans in run.  Can be passed multiple times.
-    -s --status [status]: only list tests marked as [status] in testrail.  Can be passed multiple times.
-    -a --assignedto [user]: only list tests assigned to user. Can be passed multiple times.
+--user       : Your TestRail User Name.
 
-=head3 CONFIG OPTIONS
+-j --project : desired project name.
 
-    In your \$HOME, (or the current directory, if your system has no
-    concept of a home directory) put a file called .testrailrc with
-    key=value syntax separated by newlines.
-    Valid Keys are: apiurl,user,password
+-r --run     : desired run name.
 
-=head3 CONFIG OVERRIDES
+=back
 
-    These override the config, if present.
-    If neither are used, you will be prompted.
+All mandatory options not passed with the above switches, or in your ~/.testrailrc will be prompted for.
 
-  --apiurl   [url] : full URL to get to TestRail index document
-  --password [key] : Your TestRail Password, or a valid API key (TestRail 4.2 and above).
-  --user    [name] : Your TestRail User Name.
+=head2 SEMI-OPTIONAL PARAMETERS
 
-=head2 TESTING OPTIONS:
+=over 4
 
-    --mock: don't do any real HTTP requests.
-    --help: show this output
+-p --plan       : desired plan name.  Required if the run passed is a child of a plan.
+
+-m --match      : attempt to find filenames matching the test names in the provided dir.
+
+--no-match      : attempt to find filenames that do not match test names in the provided dir.
+
+-n --no-recurse : if match (or no-match) passed, do not recurse subdirectories.
+
+-e --encoding   : Character encoding of arguments.  Defaults to UTF-8. See L<Encode::Supported> for supported encodings.
+
+=back
+
+=head2 OPTIONAL PARAMETERS
+
+=over 4
+
+-c --config     : configuration name to filter plans in run.  Can be passed multiple times.
+
+-s --status     : only list tests marked as [status] in testrail.  Can be passed multiple times.
+
+-a --assignedto : only list tests assigned to user. Can be passed multiple times.
+
+=back
+
+=head1 CONFIGURATION FILE
+
+In your \$HOME, (or the current directory, if your system has no concept of a home directory) put a file called .testrailrc with key=value syntax separated by newlines.
+Valid Keys are the same as documented by L<App::Prove::Plugin::TestRail>.
+All options specified thereby are overridden by passing the command-line switches above.
+
+=head1 MISCELLANEOUS OPTIONS:
+
+=over 4
+
+--mock : don't do any real HTTP requests. Used only by tests.
+
+--help : show this output
+
+=back
 
 =cut
 
@@ -68,88 +90,80 @@ use File::Find;
 use Cwd qw{abs_path};
 use File::Basename qw{basename};
 
-use Pod::Perldoc 3.10;
+my $opts ={};
 
-sub help {
-    @ARGV = ($0);
-    Pod::Perldoc->run();
-    exit 0;
+#Parse config file if we are missing api url/key or user
+my $homedir = my_home() || '.';
+if (-e $homedir . '/.testrailrc') {
+    $opts = TestRail::Utils::parseConfig($homedir);
 }
 
-my %opts;
-
 GetOptions(
-    'apiurl=s'        => \$opts{'apiurl'},
-    'password=s'      => \$opts{'pass'},
-    'user=s'          => \$opts{'user'},
-    'j|project=s'     => \$opts{'project'},
-    'p|plan=s'        => \$opts{'plan'},
-    'r|run=s'         => \$opts{'run'},
-    'c|config=s@'     => \$opts{'configs'},
-    's|status=s@'     => \$opts{'statuses'},
-    'a|assignedto=s@' => \$opts{'users'},
-    'mock'            => \$opts{'mock'},
-    'm|match=s'       => \$opts{'match'},
-    'no-match=s'      => \$opts{'no-match'},
-    'n|no-recurse'    => \$opts{'no-recurse'},
-    'e|encoding=s'    => \$opts{'encoding'},
-    'h|help'          => \$opts{'help'}
+    'apiurl=s'        => \$opts->{'apiurl'},
+    'password=s'      => \$opts->{'password'},
+    'user=s'          => \$opts->{'user'},
+    'j|project=s'     => \$opts->{'project'},
+    'p|plan=s'        => \$opts->{'plan'},
+    'r|run=s'         => \$opts->{'run'},
+    'c|config=s@'     => \$opts->{'configs'},
+    's|status=s@'     => \$opts->{'statuses'},
+    'a|assignedto=s@' => \$opts->{'users'},
+    'mock'            => \$opts->{'mock'},
+    'm|match=s'       => \$opts->{'match'},
+    'no-match=s'      => \$opts->{'no-match'},
+    'n|no-recurse'    => \$opts->{'no-recurse'},
+    'e|encoding=s'    => \$opts->{'encoding'},
+    'h|help'          => \$opts->{'help'}
 );
 
-if ($opts{help}) { help(); }
-if ($opts{'match'} && $opts{'no-match'}) {
+if ($opts->{help}) { TestRail::Utils::help(); }
+
+if ($opts->{'match'} && $opts->{'no-match'}) {
     print "Error! match and no-match options are mutually exclusive.\n";
     exit 255;
 }
 
+TestRail::Utils::interrogateUser($opts,qw{apiurl user password project run});
 
-#Parse config file if we are missing api url/key or user
-my $homedir = my_home() || '.';
-if (-e $homedir . '/.testrailrc' && (!$opts{apiurl} || !$opts{pass} || !$opts{user}) ) {
-    ($opts{apiurl},$opts{pass},$opts{user}) = TestRail::Utils::parseConfig($homedir,1);
-}
-
-TestRail::Utils::interrogateUser(\%opts,qw{apiurl user pass project run});
-
-if ($opts{mock}) {
+if ($opts->{mock}) {
     use Test::LWP::UserAgent::TestRailMock;
-    $opts{browser} = $Test::LWP::UserAgent::TestRailMock::mockObject;
-    $opts{debug} = 1;
+    $opts->{browser} = $Test::LWP::UserAgent::TestRailMock::mockObject;
+    $opts->{debug} = 1;
 }
 
-my $tr = TestRail::API->new($opts{apiurl},$opts{user},$opts{pass},$opts{'encoding'},$opts{'debug'});
-$tr->{'browser'} = $opts{'browser'} if $opts{'browser'};
+my $tr = TestRail::API->new($opts->{apiurl},$opts->{user},$opts->{password},$opts->{'encoding'},$opts->{'debug'});
+$tr->{'browser'} = $opts->{'browser'} if $opts->{'browser'};
 $tr->{'debug'} = 0;
 
-my $project = $tr->getProjectByName($opts{'project'});
+my $project = $tr->getProjectByName($opts->{'project'});
 if (!$project) {
-    print "No such project '$opts{project}'.\n";
+    print "No such project '$opts->{project}'.\n";
     exit 6;
 }
 
 my ($run,$plan);
 
-if ($opts{'plan'}) {
-    $plan = $tr->getPlanByName($project->{'id'},$opts{'plan'});
+if ($opts->{'plan'}) {
+    $plan = $tr->getPlanByName($project->{'id'},$opts->{'plan'});
     if (!$plan) {
-        print "No such plan '$opts{plan}'!\n";
+        print "No such plan '$opts->{plan}'!\n";
         exit 1;
     }
-    $run = $tr->getChildRunByName($plan,$opts{'run'}, $opts{'configs'});
+    $run = $tr->getChildRunByName($plan,$opts->{'run'}, $opts->{'configs'});
 } else {
-    $run = $tr->getRunByName($project->{'id'},$opts{'run'});
+    $run = $tr->getRunByName($project->{'id'},$opts->{'run'});
 }
 
 if (!$run) {
-    print "No such run '$opts{run}' matching the provided configs (if any).\n";
+    print "No such run '$opts->{run}' matching the provided configs (if any).\n";
     exit 2;
 }
 
 my ($status_ids,$user_ids);
 
 #Process statuses
-if ($opts{'statuses'}) {
-    eval { @$status_ids = $tr->statusNamesToIds(@{$opts{'statuses'}}); };
+if ($opts->{'statuses'}) {
+    eval { @$status_ids = $tr->statusNamesToIds(@{$opts->{'statuses'}}); };
     if ($@) {
         print "$@\n";
         exit 4;
@@ -157,8 +171,8 @@ if ($opts{'statuses'}) {
 }
 
 #Process assignedto ids
-if ($opts{'users'}) {
-    eval { @$user_ids = $tr->userNamesToIds(@{$opts{'users'}}); };
+if ($opts->{'users'}) {
+    eval { @$user_ids = $tr->userNamesToIds(@{$opts->{'users'}}); };
     if ($@) {
         print "$@\n";
         exit 5;
@@ -175,9 +189,9 @@ if (!$cases) {
 my @tests =  map {$_->{'title'}} @$cases;
 my @realtests;
 
-if ($opts{'match'} || $opts{'no-match'}) {
-    my $dir = $opts{'match'} ? $opts{'match'} : $opts{'no-match'};
-    if (!$opts{'no-recurse'}) {
+if ($opts->{'match'} || $opts->{'no-match'}) {
+    my $dir = $opts->{'match'} ? $opts->{'match'} : $opts->{'no-match'};
+    if (!$opts->{'no-recurse'}) {
         File::Find::find( sub { push(@realtests,$File::Find::name) if -f }, $dir );
         @tests = grep {my $real = $_; grep { basename($real) eq $_ } @tests} @realtests; #XXX if you have dups in your tree, be-ware
     } else {
@@ -185,10 +199,10 @@ if ($opts{'match'} || $opts{'no-match'}) {
         @realtests = glob("$dir/*");
         @tests = map {$^O eq 'MSWin32' ? "$dir/$_" : $_ } grep {my $fname = $_; grep { basename($_) eq $fname} @realtests } @tests;
     }
-    @tests = grep {my $otest = basename($_); scalar(grep {basename($_) eq $otest} @tests) == 0} @realtests if $opts{'no-match'}; #invert the list in this case.
+    @tests = grep {my $otest = basename($_); scalar(grep {basename($_) eq $otest} @tests) == 0} @realtests if $opts->{'no-match'}; #invert the list in this case.
 }
 
-@tests = map { abs_path($_) } @tests if $opts{'match'};
+@tests = map { abs_path($_) } @tests if $opts->{'match'};
 print join("\n",@tests)."\n" if scalar(@tests);
 exit 0;
 

+ 1 - 0
lib/App/Prove/Plugin/TestRail.pm

@@ -35,6 +35,7 @@ If \$HOME/.testrailrc exists, it will be parsed for any of these values in a new
     version=xx.xx.xx.xx
     case_per_ok=0
     step_results=sr_sys_name
+    lockname=internal_lock_name
     spawn=123
     sections=section1:section2:section3: ... :sectionN
     autoclose=0

+ 11 - 2
lib/TestRail/API.pm

@@ -2019,7 +2019,7 @@ The names referred to here are 'internal names' rather than the labels shown in
 
 =back
 
-Returns ARRAY of status IDs.
+Returns ARRAY of status IDs in the same order as the status names passed.
 
 Throws an exception in the case of one (or more) of the names not corresponding to a valid test status.
 
@@ -2029,7 +2029,16 @@ sub statusNamesToIds {
     my ($self,@names) = @_;
     confess("Object methods must be called by an instance") unless ref($self);
     confess("At least one status name must be provided") if !scalar(@names);
-    my @ret = grep {defined $_} map {my $status = $_; my @list = grep {$status->{'name'} eq $_} @names; scalar(@list) ? $status->{'id'} : undef} @{$self->getPossibleTestStatuses()};
+    my $statuses = $self->getPossibleTestStatuses();
+    my @ret;
+    foreach my $name (@names) {
+        foreach my $status (@$statuses) {
+            if ($status->{'name'} eq $name) {
+                push @ret, $status->{'id'};
+                last;
+            }
+        }
+    }
     confess("One or more status names provided does not exist in TestRail.") unless scalar(@names) == scalar(@ret);
     return @ret;
 };

+ 14 - 0
lib/TestRail/Utils.pm

@@ -12,8 +12,22 @@ package TestRail::Utils;
 use strict;
 use warnings;
 
+use Pod::Perldoc 3.10;
+
 =head1 FUNCTIONS
 
+=head2 help
+
+Print the perldoc for $0 and exit.
+
+=cut
+
+sub help {
+    @ARGV = ($0);
+    Pod::Perldoc->run();
+    exit 0;
+}
+
 =head2 userInput
 
 Wait for user input and return it.