Procházet zdrojové kódy

Implement #27, testrail-tests and it's unit tests

George S. Baugh před 10 roky
rodič
revize
5b25e97053

+ 3 - 0
Changes

@@ -3,6 +3,9 @@ Revision history for Perl module TestRail::API
 0.021 2015-04-07 TEODESIAN
     - Fix issue where getChildRuns did not return anything past first run
     - Fix issue where getChildRunByName did not perform configuration filtering correctly
+    - Add ability to filter by test status and assignedto id to getTests
+    - Add bin/testrail-tests and bin/testrail-runs
+    - Add statusNamesToIds and userNamesToIds convenience methods to TestRail::API
 
 0.020 2015-03-25 TEODESIAN
     - Add getRunsPaginated and getPlansPaginated to get around 250 hardlimit in TR results

+ 9 - 35
bin/testrail-report

@@ -15,7 +15,6 @@
 
 testrail-report - report raw TAP results to a TestRail install
 
-USAGE:
 =head2 PARAMETERS:
 
 =head3 MANDATORY PARAMETERS
@@ -48,12 +47,12 @@ This should provide sufficient uniqueness to get any run using names.
       If plans/configurations are supplied, it will attempt to create it as a child of the provided plan, and with the supplied configurations.
       If the specified run already exists, the program will simply use the existing run, and disregard the supplied testsuite_id.
 
-=head3 CONFIG OVERRIDES
+=head3 CONFIG OPTIONS
 
 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
 
-=head3 CONFIG OPTIONS
+=head3 CONFIG OVERRIDES
 
 These override the config, if present.  If neither are used, you will be prompted.
 
@@ -99,6 +98,7 @@ states which TAP can have.
 use strict;
 use warnings;
 
+use TestRail::Utils;
 use Getopt::Long;
 use Term::ANSIColor 2.01 qw(colorstrip);
 use Test::Rail::Parser;
@@ -212,32 +212,6 @@ TESTING OPTIONS:
     exit 0;
 }
 
-sub userInput {
- $| = 1;
- my $rt = <STDIN>;
- chomp $rt;
- return $rt;
-}
-
-sub parseConfig {
-    my $homedir = shift;
-    my $results = {};
-    my $arr =[];
-
-    open(my $fh, '<', $homedir . '/.testrailrc') or return (undef,undef,undef);#couldn't open!
-    while (<$fh>) {
-        chomp;
-        @$arr = split(/=/,$_);
-        if (scalar(@$arr) != 2) {
-            warn("Could not parse $_ in tlreport config\n");
-            next;
-        }
-        $results->{lc($arr->[0])} = $arr->[1];
-    }
-    close($fh);
-    return ($results->{'apiurl'},$results->{'password'},$results->{'user'});
-}
-
 #Main loop------------
 
 my ($help,$apiurl,$user,$password,$project,$run,$case_per_ok,$step_results,$mock,$configs,$plan,$version,$spawn);
@@ -264,7 +238,7 @@ if ($help) { help(); }
 #Parse config file if we are missing api url/key or user
 my $homedir = my_home() || '.';
 if (-e $homedir . '/.testrailrc' && (!$apiurl || !$password || !$user) ) {
-    ($apiurl,$password,$user) = parseConfig($homedir);
+    ($apiurl,$password,$user) = TestRail::Utils::parseConfig($homedir);
 }
 
 #XXX not even close to optimized, don't slurp in the future
@@ -316,17 +290,17 @@ if ($file) {
 #Interrogate user if they didn't provide info
 if (!$apiurl) {
     print "Type the API endpoint url for your testLink install below:\n";
-    $apiurl = userInput();
+    $apiurl = TestRail::Utils::userInput();
 }
 
 if (!$user) {
     print "Type your testLink user name below:\n";
-    $user = userInput();
+    $user = TestRail::Utils::userInput();
 }
 
 if (!$password) {
     print "Type the password for your testLink user below:\n";
-    $password = userInput();
+    $password = TestRail::Utils::userInput();
 }
 
 if (!$apiurl || !$password || !$user) {
@@ -337,13 +311,13 @@ if (!$apiurl || !$password || !$user) {
 #Interrogate user if they didn't provide info
 if (!$project) {
     print "Type the name of the project you are testing under:\n";
-    $project = userInput();
+    $project = TestRail::Utils::userInput();
 }
 
 # Interrogate user if options were not passed
 if (!$run) {
     print "Type the name of the existing run you would like to run against:\n";
-    $run = userInput();
+    $run = TestRail::Utils::userInput();
 }
 
 my $debug = 0;

+ 250 - 0
bin/testrail-tests

@@ -0,0 +1,250 @@
+#!/usr/bin/env perl
+# ABSTRACT: List tests in a TestRail run matching the provided filters
+# PODNAME: testrail-test
+
+=head1 SYNOPSIS
+
+  testrail-tests [OPTIONS] | xargs prove -PTestrail=...
+
+=head1 DESCRIPTION
+
+testrail-tests - list tests in a run matching the provided filters.
+
+=head2 PARAMETERS:
+
+=head3 MANDATORY PARAMETERS
+
+    -j --project [project]: desired project name.
+    -r --run [run]: desired run name.
+
+=head3 SEMI-OPTIONAL PARAMETERS
+
+    -p --plan [plan]: desired plan name.  Required if the run passed is a child of a plan.
+
+=head3 OPTIONAL PARAMETERS
+
+    -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.
+
+=head3 CONFIG OPTIONS
+
+    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
+
+=head3 CONFIG OVERRIDES
+
+    These override the config, if present.
+    If neither are used, you will be prompted.
+
+  --apiurl   [url] : full URL to get to TestRail index document
+  --password [key] : Your TestRail Password.
+  --user    [name] : Your TestRail User Name.
+
+=head2 TESTING OPTIONS:
+
+    --mock don't do any real HTTP requests.
+
+=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};
+
+sub help {
+    print("
+testrail-tests - list tests in a run matching the provided filters.
+
+USAGE:
+
+  testrail-tests [OPTIONS] | xargs prove -PTestrail=...
+
+PARAMETERS:
+  [MANDATORY PARAMETERS]
+    -j --project [project]: desired project name.
+    -r --run [run]: desired run name.
+
+  [SEMI-OPTIONAL PARAMETERS]
+
+    -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.
+    -n --no-recurse: if match passed, do not recurse subdirectories.
+
+  [OPTIONAL PARAMETERS]
+
+    -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.
+
+  [CONFIG OPTIONS]
+    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
+
+  [CONFIG OVERRIDES]
+    These override the config, if present.
+    If neither are used, you will be prompted.
+
+  --apiurl   [url] : full URL to get to TestRail index document
+  --password [key] : Your TestRail Password.
+  --user    [name] : Your TestRail User Name.
+
+TESTING OPTIONS:
+
+    --mock don't do any real HTTP requests.
+
+");
+    exit 0;
+}
+
+my %opts;
+
+GetOptions(
+    'apiurl'          => \$opts{'apiurl'},
+    'password'        => \$opts{'pass'},
+    'user'            => \$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'},
+    'n|no-recurse'    => \$opts{'no-recurse'}
+);
+
+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);
+}
+
+#Interrogate user if they didn't provide info
+if (!$opts{apiurl}) {
+    print "Type the API endpoint url for your testLink install below:\n";
+    $opts{apiurl} = TestRail::Utils::userInput();
+}
+
+if (!$opts{user}) {
+    print "Type your testLink user name below:\n";
+    $opts{user} = TestRail::Utils::userInput();
+}
+
+if (!$opts{pass}) {
+    print "Type the password for your testLink user below:\n";
+    $opts{pass} = TestRail::Utils::userInput();
+}
+
+if (!$opts{apiurl} || !$opts{pass} || !$opts{user}) {
+    print "ERROR: api url, username and password cannot be blank.\n";
+    exit 1;
+}
+
+#Interrogate user if they didn't provide info
+if (!$opts{project}) {
+    print "Type the name of the project you are testing under:\n";
+    $opts{project} = TestRail::Utils::userInput();
+}
+
+# Interrogate user if options were not passed
+if (!$opts{run}) {
+    print "Type the name of the existing run you would like to run against:\n";
+    $opts{run} = TestRail::Utils::userInput();
+}
+
+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{pass},$opts{'debug'});
+$tr->{'browser'} = $opts{'browser'} if $opts{'browser'};
+$tr->{'debug'} = 0;
+
+my $project = $tr->getProjectByName($opts{'project'});
+
+my ($run,$plan);
+
+if ($opts{'plan'}) {
+    $plan = $tr->getPlanByName($project->{'id'},$opts{'plan'});
+    if (!$plan) {
+        print "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) {
+    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 ($@) {
+        print "$@\n";
+        exit 4;
+    }
+}
+
+#Process assignedto ids
+if ($opts{'users'}) {
+    eval { @$user_ids = $tr->userNamesToIds(@{$opts{'users'}}); };
+    if ($@) {
+        print "$@\n";
+        exit 5;
+    }
+}
+
+my $cases = $tr->getTests($run->{'id'},$status_ids,$user_ids);
+
+if (!$cases) {
+    print "No cases in TestRail!\n";
+    exit 3;
+}
+
+my @tests =  map {$_->{'title'}} @$cases;
+my @realtests;
+
+if ($opts{'match'}) {
+    if (!$opts{'no-recurse'}) {
+        File::Find::find( sub { push(@realtests,$File::Find::name) if -f }, $opts{'match'} );
+        @tests = grep {my $real = $_; grep { basename($real) eq $_ } @tests} @realtests; #XXX if you have dups in your tree, be-ware
+    } else {
+        @tests = grep {my $fname = $_; grep { basename($_) eq $fname} glob($opts{'match'}."/*") } @tests;
+    }
+}
+@tests = map { abs_path($_) } @tests;
+print join("\n",@tests)."\n" if scalar(@tests);
+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 module.

+ 86 - 5
lib/Test/LWP/UserAgent/TestRailMock.pm

@@ -78,7 +78,7 @@ $VAR4 = bless( {
                  'content-type' => 'application/json; charset=utf-8',
                  'server' => 'Apache/2.4.7 (Ubuntu)'
                }, 'HTTP::Headers' );
-$VAR5 = '[{"id":1,"name":"teodesian","email":"teodesian@cpan.org","is_active":true}]';
+$VAR5 = '[{"id":1,"name":"teodesian","email":"teodesian@cpan.org","is_active":true},{"id":2,"name":"billy","email":"billy@witchdoctor.com","is_active":true}]';
 $mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
 
 }
@@ -932,8 +932,89 @@ $VAR4 = bless( {
                  'content-type' => 'application/json; charset=utf-8',
                  'server' => 'Apache/2.4.7 (Ubuntu)'
                }, 'HTTP::Headers' );
-$VAR5 = '[{"id":15,"case_id":8,"status_id":3,"assignedto_id":null,"run_id":22,"title":"STROGGIFY POPULATION CENTERS","type_id":6,"priority_id":4,"estimate":null,"estimate_forecast":null,"refs":null,"milestone_id":null,"custom_preconds":null,"custom_steps":null,"custom_expected":null}]';
-$mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
+$VAR5 = '[{"id":15,"case_id":8,"status_id":1,"assignedto_id":null,"run_id":22,"title":"STROGGIFY POPULATION CENTERS","type_id":6,"priority_id":4,"estimate":null,"estimate_forecast":null,"refs":null,"milestone_id":null,"custom_preconds":null,"custom_steps":null,"custom_expected":null}]';
+$mockObject->map_response(qr/\Q$VAR1\E$/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
+
+}
+
+{
+
+$VAR1 = 'index.php?/api/v2/get_tests/22&status_id=1';
+$VAR2 = '200';
+$VAR3 = 'OK';
+$VAR4 = bless( {
+                 'connection' => 'close',
+                 'x-powered-by' => 'PHP/5.5.9-1ubuntu4.5',
+                 'client-response-num' => 1,
+                 'date' => 'Tue, 23 Dec 2014 20:02:10 GMT',
+                 'client-peer' => '192.168.122.217:80',
+                 'content-length' => '276',
+                 '::std_case' => {
+                                   'client-date' => 'Client-Date',
+                                   'x-powered-by' => 'X-Powered-By',
+                                   'client-response-num' => 'Client-Response-Num',
+                                   'client-peer' => 'Client-Peer'
+                                 },
+                 'client-date' => 'Tue, 23 Dec 2014 20:02:10 GMT',
+                 'content-type' => 'application/json; charset=utf-8',
+                 'server' => 'Apache/2.4.7 (Ubuntu)'
+               }, 'HTTP::Headers' );
+$VAR5 = '[{"id":15,"case_id":8,"status_id":1,"assignedto_id":null,"run_id":22,"title":"STROGGIFY POPULATION CENTERS","type_id":6,"priority_id":4,"estimate":null,"estimate_forecast":null,"refs":null,"milestone_id":null,"custom_preconds":null,"custom_steps":null,"custom_expected":null}]';
+$mockObject->map_response(qr/\Q$VAR1\E$/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
+
+}
+
+{
+
+$VAR1 = 'index.php?/api/v2/get_tests/22&status_id=2';
+$VAR2 = '200';
+$VAR3 = 'OK';
+$VAR4 = bless( {
+                 'connection' => 'close',
+                 'x-powered-by' => 'PHP/5.5.9-1ubuntu4.5',
+                 'client-response-num' => 1,
+                 'date' => 'Tue, 23 Dec 2014 20:02:10 GMT',
+                 'client-peer' => '192.168.122.217:80',
+                 'content-length' => '276',
+                 '::std_case' => {
+                                   'client-date' => 'Client-Date',
+                                   'x-powered-by' => 'X-Powered-By',
+                                   'client-response-num' => 'Client-Response-Num',
+                                   'client-peer' => 'Client-Peer'
+                                 },
+                 'client-date' => 'Tue, 23 Dec 2014 20:02:10 GMT',
+                 'content-type' => 'application/json; charset=utf-8',
+                 'server' => 'Apache/2.4.7 (Ubuntu)'
+               }, 'HTTP::Headers' );
+$VAR5 = '[]';
+$mockObject->map_response(qr/\Q$VAR1\E$/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
+
+}
+
+{
+
+$VAR1 = 'index.php?/api/v2/get_tests/1&status_id=5';
+$VAR2 = '200';
+$VAR3 = 'OK';
+$VAR4 = bless( {
+                 'connection' => 'close',
+                 'x-powered-by' => 'PHP/5.5.9-1ubuntu4.5',
+                 'client-response-num' => 1,
+                 'date' => 'Tue, 23 Dec 2014 20:02:10 GMT',
+                 'client-peer' => '192.168.122.217:80',
+                 'content-length' => '276',
+                 '::std_case' => {
+                                   'client-date' => 'Client-Date',
+                                   'x-powered-by' => 'X-Powered-By',
+                                   'client-response-num' => 'Client-Response-Num',
+                                   'client-peer' => 'Client-Peer'
+                                 },
+                 'client-date' => 'Tue, 23 Dec 2014 20:02:10 GMT',
+                 'content-type' => 'application/json; charset=utf-8',
+                 'server' => 'Apache/2.4.7 (Ubuntu)'
+               }, 'HTTP::Headers' );
+$VAR5 = '[]';
+$mockObject->map_response(qr/\Q$VAR1\E$/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
 
 }
 
@@ -961,7 +1042,7 @@ $VAR4 = bless( {
                  'server' => 'Apache/2.4.7 (Ubuntu)'
                }, 'HTTP::Headers' );
 $VAR5 = '[{"id":15,"case_id":8,"status_id":3,"assignedto_id":null,"run_id":22,"title":"faker.test","type_id":6,"priority_id":4,"estimate":null,"estimate_forecast":null,"refs":null,"milestone_id":null,"custom_preconds":null,"custom_steps":null,"custom_expected":null}]';
-$mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
+$mockObject->map_response(qr/\Q$VAR1\E$/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
 
 }
 
@@ -987,7 +1068,7 @@ $VAR4 = bless( {
                  'content-type' => 'application/json; charset=utf-8',
                  'server' => 'Apache/2.4.7 (Ubuntu)'
                }, 'HTTP::Headers' );
-$VAR5 = '[{"id":15,"case_id":8,"status_id":3,"assignedto_id":null,"run_id":22,"title":"STORAGE TANKS SEARED","type_id":6,"priority_id":4,"estimate":null,"estimate_forecast":null,"refs":null,"milestone_id":null,"custom_preconds":null,"custom_steps":null,"custom_expected":null},{"id":15,"case_id":8,"status_id":3,"assignedto_id":null,"run_id":22,"title":"NOT SO SEARED AFTER ARR"},{"id":15,"case_id":8,"status_id":3,"assignedto_id":null,"run_id":22,"title":"skipall.test"} ]';
+$VAR5 = '[{"id":15,"case_id":8,"status_id":3,"assignedto_id":null,"run_id":22,"title":"STORAGE TANKS SEARED","type_id":6,"priority_id":4,"estimate":null,"estimate_forecast":null,"refs":null,"milestone_id":null,"custom_preconds":null,"custom_steps":null,"custom_expected":null},{"id":15,"case_id":8,"status_id":3,"assignedto_id":null,"run_id":22,"title":"NOT SO SEARED AFTER ARR"},{"id":15,"case_id":8,"status_id":3,"assignedto_id":1,"run_id":22,"title":"skipall.test"} ]';
 $mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
 
 }

+ 71 - 6
lib/TestRail/API.pm

@@ -253,6 +253,31 @@ sub getUserByEmail {
     return 0;
 }
 
+=head2 userNamesToIds(names)
+
+Convenience method to translate a list of user names to TestRail user IDs.
+
+=over 4
+
+=item ARRAY C<NAMES> - Array of user names to translate to IDs.
+
+=back
+
+Returns ARRAY of user IDs.
+
+Throws an exception in the case of one (or more) of the names not corresponding to a valid username.
+
+=cut
+
+sub userNamesToIds {
+    my ($self,@names) = @_;
+    confess("Object methods must be called by an instance") unless ref($self);
+    confess("At least one user name must be provided") if !scalar(@names);
+    my @ret = grep {defined $_} map {my $user = $_; my @list = grep {$user->{'name'} eq $_} @names; scalar(@list) ? $user->{'id'} : undef} @{$self->getUsers()};
+    confess("One or more user names provided does not exist in TestRail.") unless scalar(@names) == scalar(@ret);
+    return @ret;
+};
+
 =head1 PROJECT METHODS
 
 =head2 B<createProject (name, [description,send_announcement])>
@@ -1200,6 +1225,8 @@ sub getChildRuns {
 Returns run definition HASHREF, or false if no such run is found.
 Convenience method using getChildRuns.
 
+Will throw a fatal error if one or more of the configurations passed does not exist in the project.
+
 =cut
 
 sub getChildRunByName {
@@ -1219,10 +1246,12 @@ sub getChildRunByName {
         my ($cname);
         @pconfigs = map {$_->{'id'}} grep { $cname = $_->{'name'}; grep {$_ eq $cname} @$configurations } @$avail_configs; #Get a list of IDs from the names passed
     }
+    confess("One or more configurations passed does not exist in your project!") if defined($configurations) && (scalar(@pconfigs) != scalar(@$configurations));
+
     my $found;
     foreach my $run (@$runs) {
         next if $run->{name} ne $name;
-        next if scalar(@pconfigs) ne scalar(@{$run->{'config_ids'}});
+        next if scalar(@pconfigs) != scalar(@{$run->{'config_ids'}});
 
         #Compare run config IDs against desired, invalidate run if all conditions not satisfied
         $found = 0;
@@ -1633,27 +1662,37 @@ sub getMilestoneByID {
 
 =head1 TEST METHODS
 
-=head2 B<getTests (run_id)>
+=head2 B<getTests (run_id,status_ids,assignedto_ids)>
 
-Get tests for some run.
+Get tests for some run.  Optionally filter by provided status_ids.
 
 =over 4
 
 =item INTEGER C<RUN ID> - ID of parent run.
 
+=item ARRAYREF C<STATUS IDS> (optional) - IDs of relevant test statuses to filter by.  get with getPossibleTestStatuses.
+
+=item ARRAYREF C<ASSIGNEDTO IDS> (optional) - IDs of users assigned to test to filter by.  get with getUsers.
+
 =back
 
 Returns ARRAYREF of test definition HASHREFs.
 
-    $tr->getTests(8);
+    $tr->getTests(8,[1,2,3],[2]);
 
 =cut
 
 sub getTests {
-    my ($self,$run_id) = @_;
+    my ($self,$run_id,$status_ids,$assignedto_ids) = @_;
     confess("Object methods must be called by an instance") unless ref($self);
     confess("Run ID must be integer") unless $self->_checkInteger($run_id);
-    return $self->_doRequest("index.php?/api/v2/get_tests/$run_id");
+    confess("Status IDs must be ARRAYREF") unless !defined($status_ids) || ( reftype($status_ids) || 'undef' ) eq 'ARRAY';
+    confess("Assigned to IDs must be ARRAYREF") unless !defined($assignedto_ids) || ( reftype($assignedto_ids) || 'undef' ) eq 'ARRAY';
+    my $query_string = '';
+    $query_string = '&status_id='.join(',',@$status_ids) if defined($status_ids) && scalar(@$status_ids);
+    my $results = $self->_doRequest("index.php?/api/v2/get_tests/$run_id$query_string");
+    @$results = grep {my $aid = $_->{'assignedto_id'}; grep {defined($aid) && $aid == $_} @$assignedto_ids} @$results if defined($assignedto_ids) && scalar(@$assignedto_ids);
+    return $results;
 }
 
 =head2 B<getTestByName (run_id,name)>
@@ -1769,6 +1808,32 @@ sub getPossibleTestStatuses {
     return $self->_doRequest('index.php?/api/v2/get_statuses');
 }
 
+=head2 statusNamesToIds(names)
+
+Convenience method to translate a list of statuses to TestRail status IDs.
+The names referred to here are 'internal names' rather than the labels shown in TestRail.
+
+=over 4
+
+=item ARRAY C<NAMES> - Array of status names to translate to IDs.
+
+=back
+
+Returns ARRAY of status IDs.
+
+Throws an exception in the case of one (or more) of the names not corresponding to a valid test status.
+
+=cut
+
+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()};
+    confess("One or more status names provided does not exist in TestRail.") unless scalar(@names) == scalar(@ret);
+    return @ret;
+};
+
 =head2 B<createTestResults(test_id,status_id,comment,options,custom_options)>
 
 Creates a result entry for a test.

+ 62 - 0
lib/TestRail/Utils.pm

@@ -0,0 +1,62 @@
+# ABSTRACT: Utilities for the testrail commandline functions.
+# PODNAME: TestRail::Utils
+
+package TestRail::Utils;
+
+=head1 DESCRIPTION
+
+Utilities for the testrail commandline functions.
+
+=cut
+
+=head1 FUNCTIONS
+
+=head2 userInput
+
+Wait for user input and return it.
+
+=cut
+
+sub userInput {
+ local $| = 1;
+ my $rt = <STDIN>;
+ chomp $rt;
+ return $rt;
+}
+
+=head2 parseConfig($homedir)
+
+Parse .testrailrc in the provided homedir.
+
+Returns:
+
+ARRAY - (apiurl,password,user)
+
+=cut
+
+sub parseConfig {
+    my $homedir = shift;
+    my $results = {};
+    my $arr =[];
+
+    open(my $fh, '<', $homedir . '/.testrailrc') or return (undef,undef,undef);#couldn't open!
+    while (<$fh>) {
+        chomp;
+        @$arr = split(/=/,$_);
+        if (scalar(@$arr) != 2) {
+            warn("Could not parse $_ in tlreport config\n");
+            next;
+        }
+        $results->{lc($arr->[0])} = $arr->[1];
+    }
+    close($fh);
+    return ($results->{'apiurl'},$results->{'password'},$results->{'user'});
+}
+
+1;
+
+__END__
+
+=head1 SPECIAL THANKS
+
+Thanks to cPanel Inc, for graciously funding the creation of this module.

+ 8 - 0
t/.testrailrc

@@ -0,0 +1,8 @@
+hardee=harhar
+apiurl=http://hokum.bogus
+user=zippy
+foo=bar
+password=happy
+nugs=gravy
+slkdjf;lanl.vnjnrohtowhjtoil4j423t90ujlkf;/z;nxfl`1`~@$%^^&*()_+?<:"{}
+=====================================

+ 45 - 16
t/TestRail-API.t

@@ -4,8 +4,9 @@ use warnings;
 use TestRail::API;
 use Test::LWP::UserAgent::TestRailMock;
 
-use Test::More tests => 60;
+use Test::More tests => 68;
 use Test::Fatal;
+use Test::Deep;
 use Scalar::Util 'reftype';
 use ExtUtils::MakeMaker qw{prompt};
 
@@ -44,6 +45,13 @@ is($myuser->{'email'},$login,"Can get user by email");
 is($tr->getUserByID($myuser->{'id'})->{'id'},$myuser->{'id'},"Can get user by ID");
 is($tr->getUserByName($myuser->{'name'})->{'name'},$myuser->{'name'},"Can get user by Name");
 
+my @user_names = map {$_->{'name'}} @$userlist;
+my @user_ids = map {$_->{'id'}} @$userlist;
+my @cuser_ids = $tr->userNamesToIds(@user_names);
+cmp_deeply(\@cuser_ids,\@user_ids,"userNamesToIds functions correctly");
+isnt(exception {$tr->userNamesToIds(@user_names,'potzrebie'); }, undef, "Passing invalid user name throws exception");
+
+
 #Test PROJECT methods
 my $project_name = 'CRUSH ALL HUMANS';
 
@@ -114,8 +122,12 @@ is($tr->getPlanByID($new_plan->{'id'})->{'id'},$new_plan->{'id'},"Can get plan b
 my $prun = $new_plan->{'entries'}->[0]->{'runs'}->[0];
 is($tr->getRunByID($prun->{'id'})->{'name'},"Executing the great plan","Can get child run of plan by ID");
 is($tr->getChildRunByName($new_plan,"Executing the great plan")->{'id'},$prun->{'id'},"Can find child run of plan by name");
-isnt($tr->getChildRunByName($namePlan,"Executing the great plan",['Chrome']),0,"Getting run by name returns child runs");
-is($tr->getChildRunByName($namePlan,"Executing the great plan"),0,"Getting run by name without sufficient configuration data returns child runs");
+
+SKIP: {
+    skip("Cannot create configurations programattically in the API like in mocks",2) if !$is_mock;
+    isnt($tr->getChildRunByName($namePlan,"Executing the great plan",['Chrome']),0,"Getting run by name returns child runs");
+    is($tr->getChildRunByName($namePlan,"Executing the great plan"),0,"Getting run by name without sufficient configuration data returns child runs");
+}
 
 #Test createRunInPlan
 my $updatedPlan = $tr->createRunInPlan($new_plan->{'id'},$new_suite->{'id'},'Dynamic Plan Run');
@@ -132,6 +144,11 @@ my $resTypes = $tr->getTestResultFields();
 my $statusTypes = $tr->getPossibleTestStatuses();
 ok($resTypes,"Can get test result fields");
 ok($statusTypes,"Can get possible test statuses");
+my @status_names = map {$_->{'name'}} @$statusTypes;
+my @status_ids = map {$_->{'id'}} @$statusTypes;
+my @computed_ids = $tr->statusNamesToIds(@status_names);
+cmp_deeply(\@computed_ids,\@status_ids,"statusNamesToIds functions correctly");
+isnt(exception {$tr->statusNamesToIds(@status_names,'potzrebie'); }, undef, "Passing invalid status name throws exception");
 
 #TODO make more thorough tests for options, custom options
 my $result = $tr->createTestResults($tests->[0]->{'id'},$statusTypes->[0]->{'id'},"REAPER FORCES INBOUND");
@@ -139,6 +156,17 @@ ok(defined($result->{'id'}),"Can add test results");
 my $results = $tr->getTestResults($tests->[0]->{'id'});
 is($results->[0]->{'id'},$result->{'id'},"Can get results for test");
 
+#Test status and assignedto filtering
+my $filteredTests = $tr->getTests($new_run->{'id'},[$status_ids[0]]);
+is(scalar(@$filteredTests),1,"Test Filtering works: status id positive");
+$filteredTests = $tr->getTests($new_run->{'id'},[$status_ids[1]]);
+is(scalar(@$filteredTests),0,"Test Filtering works: status id negative");
+$filteredTests = $tr->getTests($new_run->{'id'},[$status_ids[0]],[$userlist->[0]->{'id'}]);
+is(scalar(@$filteredTests),0,"Test Filtering works: status id positive, user id negative");
+$filteredTests = $tr->getTests($new_run->{'id'},undef,[$userlist->[0]->{'id'}]);
+is(scalar(@$filteredTests),0,"Test Filtering works: status id undef, user id negative");
+#XXX there is no way to programmatically assign things :( so this will remain somewhat uncovered
+
 #Test configuration methods
 my $configs = $tr->getConfigurations($new_project->{'id'});
 my $is_arr = is(reftype($configs),'ARRAY',"Can get configurations for a project");
@@ -156,20 +184,21 @@ is_deeply(\@config_ids,$t_config_ids, "Can correctly translate Project names to
 # TestRail arbitrarily limits many calls to 250 result sets.
 # Let's make sure our getters actually get everything.
 ############################################################
-
-#Check get_plans
-foreach my $i (0..$tr->{'global_limit'}) {
-    $tr->createPlan($new_project->{'id'},$plan_name,"PETE & RE-PIOTR");
-}
-is(scalar(@{$tr->getPlans($new_project->{'id'})}),($tr->{'global_limit'} + 2),"Can get list of plans beyond ".$tr->{'global_limit'});
-
-
-#Check get_runs
-foreach my $i (0..$tr->{'global_limit'}) {
-    $tr->createRun($new_project->{'id'},$new_suite->{'id'},$run_name,"ACQUIRE CLOTHES, BOOTS AND MOTORCYCLE");
+SKIP: {
+    skip("Skipping slow tests...", 2) if $ENV{'TESTRAIL_SLOW_TESTS'};
+    #Check get_plans
+    foreach my $i (0..$tr->{'global_limit'}) {
+        $tr->createPlan($new_project->{'id'},$plan_name,"PETE & RE-PIOTR");
+    }
+    is(scalar(@{$tr->getPlans($new_project->{'id'})}),($tr->{'global_limit'} + 2),"Can get list of plans beyond ".$tr->{'global_limit'});
+
+
+    #Check get_runs
+    foreach my $i (0..$tr->{'global_limit'}) {
+        $tr->createRun($new_project->{'id'},$new_suite->{'id'},$run_name,"ACQUIRE CLOTHES, BOOTS AND MOTORCYCLE");
+    }
+    is(scalar(@{$tr->getRuns($new_project->{'id'})}),($tr->{'global_limit'} + 2),"Can get list of runs beyond ".$tr->{'global_limit'});
 }
-is(scalar(@{$tr->getRuns($new_project->{'id'})}),($tr->{'global_limit'} + 2),"Can get list of runs beyond ".$tr->{'global_limit'});
-
 ##########
 # Clean up
 ##########

+ 15 - 0
t/TestRail-Utils.t

@@ -0,0 +1,15 @@
+use strict;
+use warnings;
+
+use Test::More 'tests' => 4;
+use Test::Fatal;
+use TestRail::Utils;
+use File::Basename qw{dirname};
+
+my ($apiurl,$user,$password);
+is(exception {($apiurl,$password,$user) = TestRail::Utils::parseConfig(dirname(__FILE__))}, undef, "No exceptions thrown by parseConfig");
+is($apiurl,'http://hokum.bogus',"APIURL parse OK");
+is($user,'zippy',"USER parse OK");
+is($password, 'happy', 'PASSWORD parse OK');
+
+#Regrettably, I have yet to find a way to print to stdin without eval, so userInput will remain untested.

+ 3 - 1
t/arg_types.t

@@ -2,7 +2,7 @@ use strict;
 use warnings;
 
 use TestRail::API;
-use Test::More 'tests' => 133;
+use Test::More 'tests' => 135;
 use Test::Fatal;
 use Class::Inspector;
 use Test::LWP::UserAgent;
@@ -70,6 +70,8 @@ isnt( exception {$tr->getChildRuns() },undef,'getChildRuns returns error when no
 isnt( exception {$tr->getChildRunByName() },undef,'getChildRunByName returns error when no arguments are passed');
 isnt( exception {$tr->createRunInPlan() },undef,'createRunInPlan returns error when no arguments are passed');
 isnt( exception {$tr->translateConfigNamesToIds()}, undef,'translateConfigNamesToIds returns error when no arguments are passed');
+isnt( exception {$tr->userNamesToIds()}, undef,'userNamesToIds returns error when no arguments are passed');
+isnt( exception {$tr->statusNamesToIds()}, undef,'statusNamesToIds returns error when no arguments are passed');
 
 #1-arg functions
 is(exception {$tr->deleteCase(1)},            undef,'deleteCase returns no error when int arg passed');

+ 63 - 0
t/testrail-tests.t

@@ -0,0 +1,63 @@
+use strict;
+use warnings;
+
+use Test::More 'tests' => 18;
+
+#check plan mode
+my @args = ($^X,qw{bin/testrail-tests -j TestProject -p 'mah dubz plan' -r 'TestingSuite' -m t --config Firefox --mock --no-recurse});
+my $out = `@args`;
+is($? >> 8, 0, "Exit code OK running plan mode, no recurse");
+chomp $out;
+like($out,qr/skipall\.test$/,"Gets test correctly in plan mode, no recurse");
+
+@args = ($^X,qw{bin/testrail-tests -j TestProject -p 'mah dubz plan' -r 'TestingSuite' --config Firefox -m t --mock});
+$out = `@args`;
+is($? >> 8, 0, "Exit code OK running plan mode, recurse");
+chomp $out;
+like($out,qr/skipall\.test$/,"Gets test correctly in plan mode, recurse");
+
+#check non plan mode
+@args = ($^X,qw{bin/testrail-tests -j TestProject -r 'TestingSuite' -m t --mock --no-recurse});
+$out = `@args`;
+is($? >> 8, 0, "Exit code OK running no plan mode, no recurse");
+chomp $out;
+like($out,qr/skipall\.test$/,"Gets test correctly in no plan mode, no recurse");
+
+@args = ($^X,qw{bin/testrail-tests -j TestProject -r 'TestingSuite' -m t --mock});
+$out = `@args`;
+is($? >> 8, 0, "Exit code OK running no plan mode, recurse");
+chomp $out;
+like($out,qr/skipall\.test$/,"Gets test correctly in no plan mode, recurse");
+
+#Negative case, filtering by config
+@args = ($^X,qw{bin/testrail-tests -j TestProject -p 'mah dubz plan' -r 'TestingSuite' -m t --mock --config 'Windows 7' --config 'Internet Explorer'});
+$out = `@args`;
+isnt($? >> 8, 0, "Exit code not OK when passing invalid configs for plan");
+chomp $out;
+like($out,qr/no such run/i,"Gets test correctly in plan mode, recurse");
+
+#check assignedto filters
+@args = ($^X,qw{bin/testrail-tests -j TestProject -p 'mah dubz plan' -r 'TestingSuite' --mock --config 'Firefox' --assignedto teodesian});
+$out = `@args`;
+is($? >> 8, 0, "Exit code OK when filtering by assignment");
+like($out,qr/skipall\.test$/,"Gets test correctly when filtering by assignment");
+
+@args = ($^X,qw{bin/testrail-tests -j TestProject -p 'mah dubz plan' -r 'TestingSuite' --mock --config 'Firefox' --assignedto billy});
+$out = `@args`;
+is($? >> 8, 0, "Exit code OK when filtering by assignement");
+chomp $out;
+is($out,'',"Gets no tests correctly when filtering by wrong assignment");
+
+#check status filters
+@args = ($^X,qw{bin/testrail-tests -j TestProject -p 'mah dubz plan' -r 'TestingSuite' -m t --mock --config 'Firefox' --status 'passed'});
+$out = `@args`;
+is($? >> 8, 0, "Exit code OK when filtering by status");
+like($out,qr/skipall\.test$/,"Gets test correctly when filtering by status");
+
+@args = ($^X,qw{bin/testrail-tests -j TestProject -p 'mah dubz plan' -r 'TestingSuite' --mock --config 'Firefox' --status 'failed'});
+$out = `@args`;
+is($? >> 8, 0, "Exit code OK when filtering by status");
+chomp $out;
+is($out,'',"Gets no tests correctly when filtering by wrong status");
+
+