浏览代码

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

George S. Baugh 10 年之前
父节点
当前提交
5b25e97053
共有 11 个文件被更改,包括 615 次插入63 次删除
  1. 3 0
      Changes
  2. 9 35
      bin/testrail-report
  3. 250 0
      bin/testrail-tests
  4. 86 5
      lib/Test/LWP/UserAgent/TestRailMock.pm
  5. 71 6
      lib/TestRail/API.pm
  6. 62 0
      lib/TestRail/Utils.pm
  7. 8 0
      t/.testrailrc
  8. 45 16
      t/TestRail-API.t
  9. 15 0
      t/TestRail-Utils.t
  10. 3 1
      t/arg_types.t
  11. 63 0
      t/testrail-tests.t

+ 3 - 0
Changes

@@ -3,6 +3,9 @@ Revision history for Perl module TestRail::API
 0.021 2015-04-07 TEODESIAN
 0.021 2015-04-07 TEODESIAN
     - Fix issue where getChildRuns did not return anything past first run
     - Fix issue where getChildRuns did not return anything past first run
     - Fix issue where getChildRunByName did not perform configuration filtering correctly
     - 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
 0.020 2015-03-25 TEODESIAN
     - Add getRunsPaginated and getPlansPaginated to get around 250 hardlimit in TR results
     - 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
 testrail-report - report raw TAP results to a TestRail install
 
 
-USAGE:
 =head2 PARAMETERS:
 =head2 PARAMETERS:
 
 
 =head3 MANDATORY 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 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.
       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
 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
 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.
 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 strict;
 use warnings;
 use warnings;
 
 
+use TestRail::Utils;
 use Getopt::Long;
 use Getopt::Long;
 use Term::ANSIColor 2.01 qw(colorstrip);
 use Term::ANSIColor 2.01 qw(colorstrip);
 use Test::Rail::Parser;
 use Test::Rail::Parser;
@@ -212,32 +212,6 @@ TESTING OPTIONS:
     exit 0;
     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------------
 #Main loop------------
 
 
 my ($help,$apiurl,$user,$password,$project,$run,$case_per_ok,$step_results,$mock,$configs,$plan,$version,$spawn);
 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
 #Parse config file if we are missing api url/key or user
 my $homedir = my_home() || '.';
 my $homedir = my_home() || '.';
 if (-e $homedir . '/.testrailrc' && (!$apiurl || !$password || !$user) ) {
 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
 #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
 #Interrogate user if they didn't provide info
 if (!$apiurl) {
 if (!$apiurl) {
     print "Type the API endpoint url for your testLink install below:\n";
     print "Type the API endpoint url for your testLink install below:\n";
-    $apiurl = userInput();
+    $apiurl = TestRail::Utils::userInput();
 }
 }
 
 
 if (!$user) {
 if (!$user) {
     print "Type your testLink user name below:\n";
     print "Type your testLink user name below:\n";
-    $user = userInput();
+    $user = TestRail::Utils::userInput();
 }
 }
 
 
 if (!$password) {
 if (!$password) {
     print "Type the password for your testLink user below:\n";
     print "Type the password for your testLink user below:\n";
-    $password = userInput();
+    $password = TestRail::Utils::userInput();
 }
 }
 
 
 if (!$apiurl || !$password || !$user) {
 if (!$apiurl || !$password || !$user) {
@@ -337,13 +311,13 @@ if (!$apiurl || !$password || !$user) {
 #Interrogate user if they didn't provide info
 #Interrogate user if they didn't provide info
 if (!$project) {
 if (!$project) {
     print "Type the name of the project you are testing under:\n";
     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
 # Interrogate user if options were not passed
 if (!$run) {
 if (!$run) {
     print "Type the name of the existing run you would like to run against:\n";
     print "Type the name of the existing run you would like to run against:\n";
-    $run = userInput();
+    $run = TestRail::Utils::userInput();
 }
 }
 
 
 my $debug = 0;
 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',
                  'content-type' => 'application/json; charset=utf-8',
                  'server' => 'Apache/2.4.7 (Ubuntu)'
                  'server' => 'Apache/2.4.7 (Ubuntu)'
                }, 'HTTP::Headers' );
                }, '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));
 $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',
                  'content-type' => 'application/json; charset=utf-8',
                  'server' => 'Apache/2.4.7 (Ubuntu)'
                  'server' => 'Apache/2.4.7 (Ubuntu)'
                }, 'HTTP::Headers' );
                }, '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)'
                  'server' => 'Apache/2.4.7 (Ubuntu)'
                }, 'HTTP::Headers' );
                }, '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}]';
 $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',
                  'content-type' => 'application/json; charset=utf-8',
                  'server' => 'Apache/2.4.7 (Ubuntu)'
                  'server' => 'Apache/2.4.7 (Ubuntu)'
                }, 'HTTP::Headers' );
                }, '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));
 $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;
     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
 =head1 PROJECT METHODS
 
 
 =head2 B<createProject (name, [description,send_announcement])>
 =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.
 Returns run definition HASHREF, or false if no such run is found.
 Convenience method using getChildRuns.
 Convenience method using getChildRuns.
 
 
+Will throw a fatal error if one or more of the configurations passed does not exist in the project.
+
 =cut
 =cut
 
 
 sub getChildRunByName {
 sub getChildRunByName {
@@ -1219,10 +1246,12 @@ sub getChildRunByName {
         my ($cname);
         my ($cname);
         @pconfigs = map {$_->{'id'}} grep { $cname = $_->{'name'}; grep {$_ eq $cname} @$configurations } @$avail_configs; #Get a list of IDs from the names passed
         @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;
     my $found;
     foreach my $run (@$runs) {
     foreach my $run (@$runs) {
         next if $run->{name} ne $name;
         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
         #Compare run config IDs against desired, invalidate run if all conditions not satisfied
         $found = 0;
         $found = 0;
@@ -1633,27 +1662,37 @@ sub getMilestoneByID {
 
 
 =head1 TEST METHODS
 =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
 =over 4
 
 
 =item INTEGER C<RUN ID> - ID of parent run.
 =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
 =back
 
 
 Returns ARRAYREF of test definition HASHREFs.
 Returns ARRAYREF of test definition HASHREFs.
 
 
-    $tr->getTests(8);
+    $tr->getTests(8,[1,2,3],[2]);
 
 
 =cut
 =cut
 
 
 sub getTests {
 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("Object methods must be called by an instance") unless ref($self);
     confess("Run ID must be integer") unless $self->_checkInteger($run_id);
     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)>
 =head2 B<getTestByName (run_id,name)>
@@ -1769,6 +1808,32 @@ sub getPossibleTestStatuses {
     return $self->_doRequest('index.php?/api/v2/get_statuses');
     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)>
 =head2 B<createTestResults(test_id,status_id,comment,options,custom_options)>
 
 
 Creates a result entry for a test.
 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 TestRail::API;
 use Test::LWP::UserAgent::TestRailMock;
 use Test::LWP::UserAgent::TestRailMock;
 
 
-use Test::More tests => 60;
+use Test::More tests => 68;
 use Test::Fatal;
 use Test::Fatal;
+use Test::Deep;
 use Scalar::Util 'reftype';
 use Scalar::Util 'reftype';
 use ExtUtils::MakeMaker qw{prompt};
 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->getUserByID($myuser->{'id'})->{'id'},$myuser->{'id'},"Can get user by ID");
 is($tr->getUserByName($myuser->{'name'})->{'name'},$myuser->{'name'},"Can get user by Name");
 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
 #Test PROJECT methods
 my $project_name = 'CRUSH ALL HUMANS';
 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];
 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->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");
 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
 #Test createRunInPlan
 my $updatedPlan = $tr->createRunInPlan($new_plan->{'id'},$new_suite->{'id'},'Dynamic Plan Run');
 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();
 my $statusTypes = $tr->getPossibleTestStatuses();
 ok($resTypes,"Can get test result fields");
 ok($resTypes,"Can get test result fields");
 ok($statusTypes,"Can get possible test statuses");
 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
 #TODO make more thorough tests for options, custom options
 my $result = $tr->createTestResults($tests->[0]->{'id'},$statusTypes->[0]->{'id'},"REAPER FORCES INBOUND");
 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'});
 my $results = $tr->getTestResults($tests->[0]->{'id'});
 is($results->[0]->{'id'},$result->{'id'},"Can get results for test");
 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
 #Test configuration methods
 my $configs = $tr->getConfigurations($new_project->{'id'});
 my $configs = $tr->getConfigurations($new_project->{'id'});
 my $is_arr = is(reftype($configs),'ARRAY',"Can get configurations for a project");
 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.
 # TestRail arbitrarily limits many calls to 250 result sets.
 # Let's make sure our getters actually get everything.
 # 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
 # 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 warnings;
 
 
 use TestRail::API;
 use TestRail::API;
-use Test::More 'tests' => 133;
+use Test::More 'tests' => 135;
 use Test::Fatal;
 use Test::Fatal;
 use Class::Inspector;
 use Class::Inspector;
 use Test::LWP::UserAgent;
 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->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->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->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
 #1-arg functions
 is(exception {$tr->deleteCase(1)},            undef,'deleteCase returns no error when int arg passed');
 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");
+
+