Browse Source

Fixes #1, and #14

See Changes for more details
George S. Baugh 11 years ago
parent
commit
0dace226a2

+ 2 - 0
Changes

@@ -6,6 +6,8 @@ Revision history for Perl module TestRail::API
     - Parse TODO/SKIP messages correctly, add todo reason to the test notes
     - Parse TODO/SKIP messages correctly, add todo reason to the test notes
     - Set SKIP_ALL tests status correctly
     - Set SKIP_ALL tests status correctly
     - Add TestRail::API::getConfigurations method
     - Add TestRail::API::getConfigurations method
+    - Add TestRail::API::getChildRuns and getChildRunByName methods to extract runs from plans when passed names
+    - Add ability to pass version to testrail-report & App::Prove::Plugin::TestRail
 
 
 0.013 2015-01-04 TEODESIAN
 0.013 2015-01-04 TEODESIAN
     - Remove usage of Types::Serialiser, and use JSON::MaybeXS (odd intermittent errors on testers)
     - Remove usage of Types::Serialiser, and use JSON::MaybeXS (odd intermittent errors on testers)

+ 4 - 1
README.md

@@ -13,7 +13,6 @@ Doesn't implement every method provided (yet), just the ones *I* needed:
 * Setting test run statuses
 * Setting test run statuses
 
 
 Basically everything needed to sync up automated test runs to the test management DB.
 Basically everything needed to sync up automated test runs to the test management DB.
-TODO: alter tests so that you can keep in sync.
 
 
 > my $url = "http://some.testrail.install/";
 > my $url = "http://some.testrail.install/";
 > 
 > 
@@ -23,4 +22,8 @@ TODO: alter tests so that you can keep in sync.
 > 
 > 
 > my $apiClient = new TestRail::API($url,$user,$pass);
 > my $apiClient = new TestRail::API($url,$user,$pass);
 
 
+Also provides a prove plugin and TAP analyzer so that you can upload results on-the-fly or after it's logged to a file.
+
+TODO: alter tests so that you can keep in sync.
+
 See POD for more info.
 See POD for more info.

+ 65 - 14
bin/testrail-report

@@ -28,9 +28,23 @@ IF none of these options are provided, you will be asked to type
 these in as needed, supposing you are not redirecting input
 these in as needed, supposing you are not redirecting input
 (such as piping into this command).
 (such as piping into this command).
 
 
+=head3 SEMI-OPTIONAL PARAMETERS
+
+    --plan [someplan] : look for the provided run name within the provided plan.
+
+    --config [someconfig] : filter run by the provided configuration.
+      This option can be passed multiple times for detailed filtering.
+
+Test plans can have runs with the same name, but different configurations, which is understandably confusing.
+You can do the same outside of plans, and without configurations; but doing so is ill advised, and the only option from there is to use IDs.
+So, try not to do that if you want to use this tool, and want sanity in your Test management system.
+
+The way around this is to specify what plan and configuration you want to set results for.
+This should provide sufficient uniqueness to get to any run using names.
+
 =head3 CONFIG OVERRIDES
 =head3 CONFIG OVERRIDES
 
 
-In your \$HOME, put a file called .testrailrc with key=value
+In your $HOME, 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 OPTIONS
@@ -45,13 +59,17 @@ These override the config, if present.  If neither are used, you will be prompte
 
 
 =head3 BEHAVIOR
 =head3 BEHAVIOR
 
 
-    --case-ok      : Whether to consider each OK to correspond to a test in TestRail
+    --case-ok : Whether to consider each OK to correspond to a test in TestRail
 
 
     --step-results [name] : 'System Name' of a 'step_results' type field to set for your tests.
     --step-results [name] : 'System Name' of a 'step_results' type field to set for your tests.
 
 
 These options are mutually exclusive.  If neither is set, the
 These options are mutually exclusive.  If neither is set, the
 overall result of the test will be used as the pass/fail for the test.
 overall result of the test will be used as the pass/fail for the test.
 
 
+=head3 RESULT OPTIONS
+
+    --version : String describing the version of the system under test.
+
 =head2 PROVE PLUGIN:
 =head2 PROVE PLUGIN:
 
 
 passing -PTestRail=apiurl,user,pass,project,run to prove will
 passing -PTestRail=apiurl,user,pass,project,run to prove will
@@ -107,6 +125,25 @@ PARAMETERS:
   these in as needed, supposing you are not redirecting input
   these in as needed, supposing you are not redirecting input
   (such as piping into this command).
   (such as piping into this command).
 
 
+  [SEMI-OPTIONAL PARAMETERS]
+
+    --plan [someplan] : look for the provided run name within
+      the provided plan.
+
+    --configs [someconfigs] : filter run by the provided configuration.
+      This option can be passed multiple times for detailed filtering.
+
+  Test plans can have runs with the same name, but different
+  configurations, which is understandably confusing.  You can do the
+  same outside of plans, and without configurations; but doing so is
+  ill-advised, and the only option from there is to use IDs.  So, try
+  not to do that if you want to use this tool, and want sanity in your
+  Test Management System.
+
+  The way around this is to specify what plan and configuration
+  you want to set results for.  This should provide sufficient
+  uniqueness to get to any run using words.
+
   [CONFIG OVERRIDES]
   [CONFIG OVERRIDES]
   In your \$HOME, put a file called .testrailrc with key=value
   In your \$HOME, 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
@@ -128,6 +165,10 @@ PARAMETERS:
   These options are mutually exclusive.  If neither is set, the
   These options are mutually exclusive.  If neither is set, the
   overall result of the test will be used as the pass/fail for the test.
   overall result of the test will be used as the pass/fail for the test.
 
 
+  [RESULT OPTIONS]
+
+    --version : String describing the version of the system under test.
+
 PROVE PLUGIN:
 PROVE PLUGIN:
 
 
   passing -PTestRail=apiurl,user,pass,project,run to prove will
   passing -PTestRail=apiurl,user,pass,project,run to prove will
@@ -176,7 +217,7 @@ sub parseConfig {
 
 
 #Main loop------------
 #Main loop------------
 
 
-my ($help,$apiurl,$user,$password,$project,$run,$case_per_ok,$step_results,$mock);
+my ($help,$apiurl,$user,$password,$project,$run,$case_per_ok,$step_results,$mock,$configs,$plan,$version);
 
 
 #parse switches
 #parse switches
 GetOptions(
 GetOptions(
@@ -188,6 +229,9 @@ GetOptions(
     'case-ok'        => \$case_per_ok,
     'case-ok'        => \$case_per_ok,
     'step-results=s' => \$step_results,
     'step-results=s' => \$step_results,
     'mock'           => \$mock,
     'mock'           => \$mock,
+    'config=s@'      => \$configs,
+    'plan=s'         => \$plan,
+    'version=s'      => \$version,
     'help'           => \$help
     'help'           => \$help
 );
 );
 
 
@@ -285,21 +329,28 @@ if ($mock) {
     $debug = 1;
     $debug = 1;
 }
 }
 
 
+my $result_options = undef;
+$result_options = {'version' => $version} if $version;
+
 my $tap;
 my $tap;
 foreach my $phil (@files) {
 foreach my $phil (@files) {
     print "Processing $phil...\n";
     print "Processing $phil...\n";
     $tap = Test::Rail::Parser->new({
     $tap = Test::Rail::Parser->new({
-        'tap'                 => $phil,
-        'apiurl'              => $apiurl,
-        'user'                => $user,
-        'pass'                => $password,
-        'run'                 => $run,
-        'project'             => $project,
-        'case_per_ok'         => $case_per_ok,
-        'step_results'        => $step_results,
-        'debug'               => $debug,
-        'browser'             => $browser,
-        'merge'               => 1
+        'tap'          => $phil,
+        'apiurl'       => $apiurl,
+        'user'         => $user,
+        'pass'         => $password,
+        'run'          => $run,
+        'project'      => $project,
+        'case_per_ok'  => $case_per_ok,
+        'step_results' => $step_results,
+        'debug'        => $debug,
+        'browser'      => $browser,
+        'version'      => $version,
+        'plan'         => $plan,
+        'configs'      => $configs,
+        'result_options' => $result_options,
+        'merge'        => 1
     });
     });
     $tap->run();
     $tap->run();
 
 

+ 32 - 19
lib/App/Prove/Plugin/TestRail.pm

@@ -9,7 +9,7 @@ use utf8;
 
 
 =head1 SYNOPSIS
 =head1 SYNOPSIS
 
 
-`prove -PTestRail='http://some.testlink.install/,someUser,somePassword,TestProject,TestRun' sometest.t`
+`prove -PTestRail='apiurl=http://some.testlink.install/,user=someUser,password=somePassword,project=TestProject,run=TestRun,plan=TestPlan,configs=Config1:Config2:Config3,version=0.014' sometest.t`
 
 
 =cut
 =cut
 
 
@@ -17,7 +17,8 @@ use utf8;
 
 
 Prove plugin to upload test results to TestRail installations.
 Prove plugin to upload test results to TestRail installations.
 
 
-Accepts input in the standard Prove plugin fashion (-Ppluginname=value,value,value...), but will also parse a config file.
+Accepts input in the standard Prove plugin fashion (-Ppluginname='key=value,key=value,key=value...'), but will also parse a config file.
+When fed in prove plugin style, key=value input is expected.
 
 
 If ~/.testrailrc exists, it will be parsed for any of these values in a newline separated key=value list.  Example:
 If ~/.testrailrc exists, it will be parsed for any of these values in a newline separated key=value list.  Example:
 
 
@@ -26,10 +27,14 @@ If ~/.testrailrc exists, it will be parsed for any of these values in a newline
     password=superS3cret
     password=superS3cret
     project=TestProject
     project=TestProject
     run=TestRun
     run=TestRun
+    plan=GosPlan
+    configs=config1:config2:config3: ... :configN
+    version=xx.xx.xx.xx
     case_per_ok=0
     case_per_ok=0
     step_results=sr_sys_name
     step_results=sr_sys_name
 
 
-Be aware that if you do so, it will look for any unsatisfied arguments in the order of their appearance above.
+Note that passing configurations as filters for runs inside of plans are separated by colons.
+Values passed in via query string will override values in ~/.testrailrc.
 
 
 =head1 OVERRIDDEN METHODS
 =head1 OVERRIDDEN METHODS
 
 
@@ -47,27 +52,35 @@ sub load {
 
 
     my $app = $p->{app_prove};
     my $app = $p->{app_prove};
     my $args = $p->{'args'};
     my $args = $p->{'args'};
-
-    $apiurl   //= shift @$args;
-    $user     //= shift @$args;
-    $password //= shift @$args;
-    $project  //= shift @$args;
-    $run      //= shift @$args;
-
-    $case_per_ok  //= shift @$args;
-    $step_results //= shift @$args;
+    my $params = {};
+
+    my @kvp = ();
+    my ($key,$value);
+    foreach my $arg (@$args) {
+        @kvp = split(/=/,$arg);
+        if (scalar(@kvp) < 2) {
+            print "Unrecognized Argument '$arg' to App::Prove::Plugin::Testrail, ignoring\n";
+            next;
+        }
+        $key = shift @kvp;
+        $value = join('',@kvp);
+        $params->{$key} = $value;
+    }
 
 
     $app->harness('Test::Rail::Harness');
     $app->harness('Test::Rail::Harness');
     $app->merge(1);
     $app->merge(1);
 
 
     #XXX I can't figure out for the life of me any other way to pass this data. #YOLO
     #XXX I can't figure out for the life of me any other way to pass this data. #YOLO
-    $ENV{'TESTRAIL_APIURL'} = $apiurl;
-    $ENV{'TESTRAIL_USER'}   = $user;
-    $ENV{'TESTRAIL_PASS'}   = $password;
-    $ENV{'TESTRAIL_PROJ'}   = $project;
-    $ENV{'TESTRAIL_RUN'}    = $run;
-    $ENV{'TESTRAIL_CASEOK'} = $case_per_ok;
-    $ENV{'TESTRAIL_STEPS'}  = $step_results;
+    $ENV{'TESTRAIL_APIURL'}  = $params->{apiurl};
+    $ENV{'TESTRAIL_USER'}    = $params->{user};
+    $ENV{'TESTRAIL_PASS'}    = $params->{password};
+    $ENV{'TESTRAIL_PROJ'}    = $params->{project};
+    $ENV{'TESTRAIL_RUN'}     = $params->{run};
+    $ENV{'TESTRAIL_PLAN'}    = $params->{plan};
+    $ENV{'TESTRAIL_CONFIGS'} = $params->{configs};
+    $ENV{'TESTRAIL_VERSION'} = $params->{version};
+    $ENV{'TESTRAIL_CASEOK'}  = $params->{case_per_ok};
+    $ENV{'TESTRAIL_STEPS'}   = $params->{step_results};
     return $class;
     return $class;
 }
 }
 
 

+ 56 - 2
lib/Test/LWP/UserAgent/TestRailMock.pm

@@ -636,6 +636,34 @@ $mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4,
 
 
 }
 }
 
 
+{
+
+$VAR1 = 'index.php?/api/v2/get_run/24';
+$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' => '654',
+                 '::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":22,"suite_id":9,"name":"Executing the great plan","description":"ACQUIRE CLOTHES, BOOTS AND MOTORCYCLE","milestone_id":null,"assignedto_id":null,"include_all":true,"is_completed":false,"completed_on":null,"config":null,"config_ids":[],"passed_count":0,"blocked_count":0,"untested_count":1,"retest_count":0,"failed_count":0,"custom_status1_count":0,"custom_status2_count":0,"custom_status3_count":0,"custom_status4_count":0,"custom_status5_count":0,"custom_status6_count":0,"custom_status7_count":0,"project_id":9,"plan_id":null,"created_on":1419364929,"created_by":1,"url":"http:\\/\\/testrail.local\\/\\/index.php?\\/runs\\/view\\/22"}';
+$mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
+
+}
+
+
 {
 {
 
 
 $VAR1 = 'index.php?/api/v2/get_run/1';
 $VAR1 = 'index.php?/api/v2/get_run/1';
@@ -828,7 +856,7 @@ $mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4,
 
 
 {
 {
 
 
-$VAR1 = 'index.php?/api/v2/get_plans/9';
+$VAR1 = 'index.php?/api/v2/get_plans/10';
 $VAR2 = '200';
 $VAR2 = '200';
 $VAR3 = 'OK';
 $VAR3 = 'OK';
 $VAR4 = bless( {
 $VAR4 = bless( {
@@ -875,7 +903,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":23,"name":"GosPlan","description":"Soviet 5-year agriculture plan to liquidate Kulaks","milestone_id":8,"assignedto_id":null,"is_completed":false,"completed_on":null,"passed_count":0,"blocked_count":0,"untested_count":1,"retest_count":0,"failed_count":0,"custom_status1_count":0,"custom_status2_count":0,"custom_status3_count":0,"custom_status4_count":0,"custom_status5_count":0,"custom_status6_count":0,"custom_status7_count":0,"project_id":9,"created_on":1419364930,"created_by":1,"url":"http:\\/\\/testrail.local\\/\\/index.php?\\/plans\\/view\\/23","entries":[{"id":"271443a5-aacf-467e-8993-b4f7001195cf","suite_id":9,"name":"Executing the great plan","runs":[{"id":24,"suite_id":9,"name":"Executing the great plan","description":null,"milestone_id":8,"assignedto_id":null,"include_all":true,"is_completed":false,"completed_on":null,"passed_count":0,"blocked_count":0,"untested_count":1,"retest_count":0,"failed_count":0,"custom_status1_count":0,"custom_status2_count":0,"custom_status3_count":0,"custom_status4_count":0,"custom_status5_count":0,"custom_status6_count":0,"custom_status7_count":0,"project_id":9,"plan_id":23,"entry_index":1,"entry_id":"271443a5-aacf-467e-8993-b4f7001195cf","config":null,"config_ids":[],"url":"http:\\/\\/testrail.local\\/\\/index.php?\\/runs\\/view\\/24"}]}]}';
+$VAR5 = '{"id":23,"name":"GosPlan","description":"Soviet 5-year agriculture plan to liquidate Kulaks","milestone_id":8,"assignedto_id":null,"is_completed":false,"completed_on":null,"passed_count":0,"blocked_count":0,"untested_count":1,"retest_count":0,"failed_count":0,"custom_status1_count":0,"custom_status2_count":0,"custom_status3_count":0,"custom_status4_count":0,"custom_status5_count":0,"custom_status6_count":0,"custom_status7_count":0,"project_id":9,"created_on":1419364930,"created_by":1,"url":"http:\\/\\/testrail.local\\/\\/index.php?\\/plans\\/view\\/23","entries":[{"id":"271443a5-aacf-467e-8993-b4f7001195cf","suite_id":9,"name":"Executing the great plan","runs":[{"id":1,"suite_id":9,"name":"Executing the great plan","description":null,"milestone_id":8,"assignedto_id":null,"include_all":true,"is_completed":false,"completed_on":null,"passed_count":0,"blocked_count":0,"untested_count":1,"retest_count":0,"failed_count":0,"custom_status1_count":0,"custom_status2_count":0,"custom_status3_count":0,"custom_status4_count":0,"custom_status5_count":0,"custom_status6_count":0,"custom_status7_count":0,"project_id":9,"plan_id":23,"entry_index":1,"entry_id":"271443a5-aacf-467e-8993-b4f7001195cf","config":"testConfig","config_ids":[1],"url":"http:\\/\\/testrail.local\\/\\/index.php?\\/runs\\/view\\/24"}]}]}';
 $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));
 
 
 }
 }
@@ -1369,5 +1397,31 @@ $mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4,
 
 
 }
 }
 
 
+{
+
+$VAR1 = 'index.php?/api/v2/get_configs/10';
+$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:12 GMT',
+                 'client-peer' => '192.168.122.217:80',
+                 'content-length' => '0',
+                 '::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:12 GMT',
+                 'content-type' => 'application/json; charset=utf-8',
+                 'server' => 'Apache/2.4.7 (Ubuntu)'
+               }, 'HTTP::Headers' );
+$VAR5 = '["testConfig"]';
+$mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
+
+}
 
 
 1;
 1;

+ 13 - 2
lib/Test/Rail/Harness.pm

@@ -46,8 +46,19 @@ sub make_parser {
     $args->{'pass'}    = $ENV{'TESTRAIL_PASS'};
     $args->{'pass'}    = $ENV{'TESTRAIL_PASS'};
     $args->{'project'} = $ENV{'TESTRAIL_PROJ'};
     $args->{'project'} = $ENV{'TESTRAIL_PROJ'};
     $args->{'run'}     = $ENV{'TESTRAIL_RUN'};
     $args->{'run'}     = $ENV{'TESTRAIL_RUN'};
-    $args->{'case_per_ok'}  = $ENV{'TESTRAIL_CASEOK'};
-    $args->{'step_results'} = $ENV{'TESTRAIL_STEPS'};
+    $args->{'plan'}    = $ENV{'TESTRAIL_PLAN'};
+    my @configs =  split(/:/,$ENV{'TESTRAIL_CONFIGS'}) if $ENV{'TESTRAIL_CONFIGS'};
+    $args->{'configs'} = \@configs;
+    $args->{'result_options'} = {'version' => $ENV{'TESTRAIL_VERSION'}} if $ENV{'TESTRAIL_VERSION'};
+    $args->{'case_per_ok'}    = $ENV{'TESTRAIL_CASEOK'};
+    $args->{'step_results'}   = $ENV{'TESTRAIL_STEPS'};
+
+    #for Testability of plugin
+    if ($ENV{'TESTRAIL_MOCKED'}) {
+        use Test::LWP::UserAgent::TestRailMock;
+        $args->{'debug'} = 1;
+        $args->{'browser'} = $Test::LWP::UserAgent::TestRailMock::mockObject;
+    }
 
 
     $self->SUPER::_make_callback( 'parser_args', $args, $job->as_array_ref );
     $self->SUPER::_make_callback( 'parser_args', $args, $job->as_array_ref );
     my $parser = $self->SUPER::_construct( $self->SUPER::parser_class, $args );
     my $parser = $self->SUPER::_construct( $self->SUPER::parser_class, $args );

+ 27 - 6
lib/Test/Rail/Parser.pm

@@ -100,6 +100,8 @@ sub new {
         'project_id'   => delete $opts->{'project_id'},
         'project_id'   => delete $opts->{'project_id'},
         'step_results' => delete $opts->{'step_results'},
         'step_results' => delete $opts->{'step_results'},
         'case_per_ok'  => delete $opts->{'case_per_ok'},
         'case_per_ok'  => delete $opts->{'case_per_ok'},
+        'plan'         => delete $opts->{'plan'},
+        'configs'      => delete $opts->{'configs'},
         #Stubs for extension by subclassers
         #Stubs for extension by subclassers
         'result_options'        => delete $opts->{'result_options'},
         'result_options'        => delete $opts->{'result_options'},
         'result_custom_options' => delete $opts->{'result_custom_options'}
         'result_custom_options' => delete $opts->{'result_custom_options'}
@@ -143,18 +145,37 @@ sub new {
     $tropts->{'todo_fail'} = $todof[0];
     $tropts->{'todo_fail'} = $todof[0];
     $tropts->{'todo_pass'} = $todop[0];
     $tropts->{'todo_pass'} = $todop[0];
 
 
-    #Grab suite from run
+    #Grab run
     my $run_id = $tropts->{'run_id'};
     my $run_id = $tropts->{'run_id'};
+    my $run;
+
+    #TODO check if configs passed are defined for project
+
     if ($tropts->{'run'}) {
     if ($tropts->{'run'}) {
-        my $run = $tr->getRunByName($tropts->{'project_id'},$tropts->{'run'});
-        if (defined($run) && (reftype($run) || 'undef') eq 'HASH') {
-            $tropts->{'run'} = $run;
-            $tropts->{'run_id'} = $run->{'id'};
+        if ($tropts->{'plan'}) {
+            #Attempt to find run, filtered by configurations
+            my $plan = $tr->getPlanByName($tropts->{'project_id'},$tropts->{'plan'});
+            if ($plan) {
+                $tropts->{'plan'} = $plan; #XXX Save for later just in case?
+                $run = $tr->getChildRunByName($plan,$tropts->{'run'},$tropts->{'configs'}); #Find plan filtered by configs
+                if (defined($run) && (reftype($run) || 'undef') eq 'HASH') {
+                    $tropts->{'run'} = $run;
+                    $tropts->{'run_id'} = $run->{'id'};
+                }
+            } else {
+                confess("Could not find plan ".$tropts->{'plan'}." in provided project!");
+            }
+        } else {
+            $run = $tr->getRunByName($tropts->{'project_id'},$tropts->{'run'});
+            if (defined($run) && (reftype($run) || 'undef') eq 'HASH') {
+                $tropts->{'run'} = $run;
+                $tropts->{'run_id'} = $run->{'id'};
+            }
         }
         }
     } else {
     } else {
         $tropts->{'run'} = $tr->getRunByID($run_id);
         $tropts->{'run'} = $tr->getRunByID($run_id);
     }
     }
-    confess("No run ID provided, and no run with specified name exists!") if !$tropts->{'run_id'};
+    confess("No run ID provided, and no run with specified name exists in provided project/plan!") if !$tropts->{'run_id'};
 
 
     $self = $class->SUPER::new($opts);
     $self = $class->SUPER::new($opts);
     if (defined($self->{'_iterator'}->{'command'}) && reftype($self->{'_iterator'}->{'command'}) eq 'ARRAY' ) {
     if (defined($self->{'_iterator'}->{'command'}) && reftype($self->{'_iterator'}->{'command'}) eq 'ARRAY' ) {

+ 82 - 5
lib/TestRail/API.pm

@@ -82,6 +82,7 @@ sub new {
         flattree         => [],
         flattree         => [],
         user_cache       => [],
         user_cache       => [],
         type_cache       => [],
         type_cache       => [],
+        configurations   => undef,
         tr_fields        => undef,
         tr_fields        => undef,
         default_request  => undef,
         default_request  => undef,
         browser          => new LWP::UserAgent()
         browser          => new LWP::UserAgent()
@@ -1109,6 +1110,75 @@ sub getRunByID {
     return $self->_doRequest("index.php?/api/v2/get_run/$run_id");
     return $self->_doRequest("index.php?/api/v2/get_run/$run_id");
 }
 }
 
 
+=head1 RUN AS CHILD OF PLAN METHODS
+
+=head2 B<getChildRuns(plan)>
+
+Extract the child runs from a plan.  Convenient, as the structure of this hash is deep, and correct error handling can be tedious.
+
+=over 4
+
+=item HASHREF C<PLAN> - Test Plan definition HASHREF returned by any of the PLAN methods below.
+
+=back
+
+Returns ARRAYREF of run definition HASHREFs.  Returns undef upon failure to extract the data.
+
+=cut
+
+sub getChildRuns {
+    my ($self,$plan) = @_;
+    confess("Object methods must be called by an instance") unless ref($self);
+    confess("Plan must be HASHREF") unless defined($plan) && (reftype($plan) || 'undef') eq 'HASH';
+    return undef unless defined($plan->{'entries'}) && (reftype($plan->{'entries'}) || 'undef') eq 'ARRAY';
+    return undef unless defined($plan->{'entries'}->[0]->{'runs'}) && (reftype($plan->{'entries'}->[0]->{'runs'}) || 'undef') eq 'ARRAY';
+    return $plan->{'entries'}->[0]->{'runs'};
+}
+
+=head2 B<getChildRunByName(plan,name,configurations)>
+
+=over 4
+
+=item HASHREF C<PLAN> - Test Plan definition HASHREF returned by any of the PLAN methods below.
+
+=item STRING C<NAME> - Name of run to search for within plan.
+
+=item ARRAYREF C<CONFIGURATIONS> (optional) - Names of configurations to filter runs by.
+
+=back
+
+Returns run definition HASHREF, or false if no such run is found.
+Convenience method using getChildRuns.
+
+=cut
+
+sub getChildRunByName {
+    my ($self,$plan,$name,$configurations) = @_;
+    confess("Object methods must be called by an instance") unless ref($self);
+    confess("Plan must be HASHREF") unless defined($plan) && (reftype($plan) || 'undef') eq 'HASH';
+    confess("Run name must be STRING") unless $self->_checkString($name);
+    confess("Configurations must be ARRAYREF") unless !defined($configurations) || (reftype($configurations) || 'undef') eq 'ARRAY';
+    my $runs = $self->getChildRuns($plan);
+    return 0 if !defined($runs);
+
+    my @pconfigs = ();
+
+    #Figure out desired config IDs
+    if (defined $configurations) {
+        my $avail_configs = $self->getConfigurations($plan->{'project_id'});
+        my ($cname,$cid,@rconfigs);
+        @pconfigs = map {$_->{'id'}} grep { $cname = $_->{'name'}; grep {$_ eq $cname} @$configurations } @$avail_configs; #Get a list of IDs from the names passed
+    }
+    foreach my $run (@$runs) {
+        #Compare run config IDs against desired, invalidate run if all conditions not satisfied
+        foreach my $cid (@{$run->{'config_ids'}}) {
+            next unless List::Util::all {$_ eq $cid} @pconfigs;
+        }
+        return $run if $run->{name} eq $name;
+    }
+    return 0;
+}
+
 =head1 PLAN METHODS
 =head1 PLAN METHODS
 
 
 =head2 B<createPlan (project_id,name,description,milestone_id,entries)>
 =head2 B<createPlan (project_id,name,description,milestone_id,entries)>
@@ -1185,9 +1255,9 @@ sub deletePlan {
     return $result;
     return $result;
 }
 }
 
 
-=head2 B<getPlans (project_id)>
+=head2 B<getPlans (project_id,get_runs)>
 
 
-Deletes specified plan.
+Gets specified test plans.
 
 
 =over 4
 =over 4
 
 
@@ -1199,6 +1269,9 @@ Returns ARRAYREF of plan definition HASHREFs.
 
 
     $tr->getPlans(8);
     $tr->getPlans(8);
 
 
+Does not contain any information about child test runs.
+Use getRunByID or getRunByName if you want that, in particular if you are interested in using getChildRunByName.
+
 =cut
 =cut
 
 
 sub getPlans {
 sub getPlans {
@@ -1234,7 +1307,9 @@ sub getPlanByName {
     my $plans = $self->getPlans($project_id);
     my $plans = $self->getPlans($project_id);
     return -500 if !$plans || (reftype($plans) || 'undef') ne 'ARRAY';
     return -500 if !$plans || (reftype($plans) || 'undef') ne 'ARRAY';
     foreach my $plan (@$plans) {
     foreach my $plan (@$plans) {
-        return $plan if $plan->{'name'} eq $name;
+        if ($plan->{'name'} eq $name) {
+            return $self->getPlanByID($plan->{'id'});
+        }
     }
     }
     return 0;
     return 0;
 }
 }
@@ -1622,7 +1697,7 @@ sub createTestResults {
     return $self->_doRequest("index.php?/api/v2/add_result/$test_id",'POST',$stuff);
     return $self->_doRequest("index.php?/api/v2/add_result/$test_id",'POST',$stuff);
 }
 }
 
 
-=head2 B<getTestResults(test_id,limit)>
+=head2 B<getTestResults(test_id,limit,offset)>
 
 
 Get the recorded results for desired test, limiting output to 'limit' entries.
 Get the recorded results for desired test, limiting output to 'limit' entries.
 
 
@@ -1673,7 +1748,9 @@ sub getConfigurations {
     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("Test ID must be positive integer") unless $self->_checkInteger($project_id);
     confess("Test ID must be positive integer") unless $self->_checkInteger($project_id);
     my $url = "index.php?/api/v2/get_configs/$project_id";
     my $url = "index.php?/api/v2/get_configs/$project_id";
-    return $self->_doRequest($url);
+    return $self->{'configurations'} if $self->{'configurations'}; #cache this since we can't change it with the API
+    $self->{'configurations'} = $self->_doRequest($url);
+    return $self->{'configurations'};
 }
 }
 
 
 =head1 STATIC METHODS
 =head1 STATIC METHODS

+ 37 - 0
t/App-Prove-Plugin-Testrail.t

@@ -0,0 +1,37 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+
+use Test::More 'tests' => 2;
+use Test::Fatal;
+use App::Prove;
+use App::Prove::Plugin::TestRail;
+use IO::Capture::Stdout;
+
+my $capture_out = IO::Capture::Stdout->new();
+
+#silence
+sub do_run {
+    my ($prove) = @_;
+    $capture_out->start;
+    $prove->run();
+    $capture_out->stop;
+}
+
+#I'm the secret squirrel
+$ENV{'TESTRAIL_MOCKED'} = 1;
+
+#Test the same sort of data as would come from the Test::Rail::Parser case
+my $prove = App::Prove->new();
+$prove->process_args("-PTestRail=apiurl=http://some.testlink.install/,user=someUser,password=somePassword,project=TestProject,run=TestingSuite,version=0.014,case_per_ok=1",'t/fake.test');
+
+is (exception {do_run($prove)},undef,"Running TR parser case via plugin functions");
+
+#Check that plan, configs and version also make it through
+$prove = App::Prove->new();
+$prove->process_args("-PTestRail=apiurl=http://some.testlink.install/,user=someUser,password=somePassword,project=TestProject,run=Executing the great plan,version=0.014,case_per_ok=1,plan=GosPlan,configs=testConfig",'t/fake.test');
+
+is (exception {do_run($prove)},undef,"Running TR parser case via plugin functions works with configs/plans");
+
+

+ 10 - 2
t/TestRail-API.t

@@ -4,7 +4,7 @@ use warnings;
 use TestRail::API;
 use TestRail::API;
 use Test::LWP::UserAgent::TestRailMock;
 use Test::LWP::UserAgent::TestRailMock;
 
 
-use Test::More tests => 52;
+use Test::More tests => 55;
 use Test::Fatal;
 use Test::Fatal;
 use Scalar::Util 'reftype';
 use Scalar::Util 'reftype';
 use ExtUtils::MakeMaker qw{prompt};
 use ExtUtils::MakeMaker qw{prompt};
@@ -111,9 +111,16 @@ my $new_plan = $tr->createPlan($new_project->{'id'},$plan_name,"Soviet 5-year ag
 is($new_plan->{'name'},$plan_name,"Can create new plan");
 is($new_plan->{'name'},$plan_name,"Can create new plan");
 
 
 ok($tr->getPlans($new_project->{'id'}),"Can get list of plans");
 ok($tr->getPlans($new_project->{'id'}),"Can get list of plans");
-is($tr->getPlanByName($new_project->{'id'},$plan_name)->{'name'},$plan_name,"Can get plan by name");
+my $namePlan = $tr->getPlanByName($new_project->{'id'},$plan_name);
+is($namePlan->{'name'},$plan_name,"Can get plan by name");
 is($tr->getPlanByID($new_plan->{'id'})->{'id'},$new_plan->{'id'},"Can get plan by ID");
 is($tr->getPlanByID($new_plan->{'id'})->{'id'},$new_plan->{'id'},"Can get plan by ID");
 
 
+#Get runs per plan, create runs in plan
+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");
+is($tr->getChildRunByName($namePlan,"Executing the great plan")->{'id'},$prun->{'id'},"Getting run by name returns child runs");
+
 #Test TEST/RESULT methods
 #Test TEST/RESULT methods
 my $tests = $tr->getTests($new_run->{'id'});
 my $tests = $tr->getTests($new_run->{'id'});
 ok($tests,"Can get tests");
 ok($tests,"Can get tests");
@@ -125,6 +132,7 @@ 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");
 
 
+#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");
 ok(defined($result->{'id'}),"Can add test results");
 ok(defined($result->{'id'}),"Can add test results");
 my $results = $tr->getTestResults($tests->[0]->{'id'});
 my $results = $tr->getTestResults($tests->[0]->{'id'});

+ 6 - 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' => 115;
+use Test::More 'tests' => 120;
 use Test::Fatal;
 use Test::Fatal;
 use Class::Inspector;
 use Class::Inspector;
 use Test::LWP::UserAgent;
 use Test::LWP::UserAgent;
@@ -63,6 +63,8 @@ isnt( exception {$tr->getRuns() },undef,'getRuns returns error when no arguments
 isnt( exception {$tr->getPlans() },undef,'getPlans returns error when no arguments are passed');
 isnt( exception {$tr->getPlans() },undef,'getPlans returns error when no arguments are passed');
 isnt( exception {$tr->getMilestones() },undef,'getMilestones returns error when no arguments are passed');
 isnt( exception {$tr->getMilestones() },undef,'getMilestones returns error when no arguments are passed');
 isnt( exception {$tr->getConfigurations() },undef,'getConfigurations returns error when no arguments are passed');
 isnt( exception {$tr->getConfigurations() },undef,'getConfigurations returns error when no arguments are passed');
+isnt( exception {$tr->getChildRuns() },undef,'getChildRuns returns error when no arguments are passed');
+isnt( exception {$tr->getChildRunByName() },undef,'getChildRunByName 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');
@@ -93,6 +95,7 @@ is(exception {$tr->createProject('zippy')},   undef,'createProject returns no er
 is(exception {$tr->getTestResults(1)},        undef,'getTestResults with 1 arg returns no error');
 is(exception {$tr->getTestResults(1)},        undef,'getTestResults with 1 arg returns no error');
 is(exception {$tr->getMilestoneByID(1)},      undef,'getMilestoneByID with 1 arg returns no error');
 is(exception {$tr->getMilestoneByID(1)},      undef,'getMilestoneByID with 1 arg returns no error');
 is(exception {$tr->getConfigurations(1)},     undef,'getConfigurations with 1 arg returns no error');
 is(exception {$tr->getConfigurations(1)},     undef,'getConfigurations with 1 arg returns no error');
+is(exception {$tr->getChildRuns({}) },        undef,'getChildRuns returns no error when 1 argument passed');
 
 
 isnt(exception {$tr->createCase(1)}, undef,'createCase with 1 arg returns error');
 isnt(exception {$tr->createCase(1)}, undef,'createCase with 1 arg returns error');
 isnt(exception {$tr->createMilestone(1)}, undef,'createMilestone with 1 arg returns error');
 isnt(exception {$tr->createMilestone(1)}, undef,'createMilestone with 1 arg returns error');
@@ -110,6 +113,7 @@ isnt(exception {$tr->getSectionByName(1)}, undef,'getSectionByName with 1 arg re
 isnt(exception {$tr->getSections(1)}, undef,'getSections with 1 arg returns error');
 isnt(exception {$tr->getSections(1)}, undef,'getSections with 1 arg returns error');
 isnt(exception {$tr->getTestByName(1)}, undef,'getTestByName with 1 arg returns error');
 isnt(exception {$tr->getTestByName(1)}, undef,'getTestByName with 1 arg returns error');
 isnt(exception {$tr->getTestSuiteByName(1)}, undef,'getTestSuiteByName with 1 arg returns error');
 isnt(exception {$tr->getTestSuiteByName(1)}, undef,'getTestSuiteByName with 1 arg returns error');
+isnt(exception {$tr->getChildRunByName({}) },undef,'getChildRunByName returns error when 1 argument passed');
 
 
 #2 arg functions
 #2 arg functions
 is(exception {$tr->createMilestone(1,'whee')}, undef,'createMilestone with 2 args returns no error');
 is(exception {$tr->createMilestone(1,'whee')}, undef,'createMilestone with 2 args returns no error');
@@ -123,6 +127,7 @@ is(exception {$tr->getSections(1,1)}, undef,'getSections with 2 args returns no
 is(exception {$tr->getTestByName(1,'poo')}, undef,'getTestByName with 2 args returns no error');
 is(exception {$tr->getTestByName(1,'poo')}, undef,'getTestByName with 2 args returns no error');
 is(exception {$tr->getTestSuiteByName(1,'zap')}, undef,'getTestSuiteByName with 2 args returns no error');
 is(exception {$tr->getTestSuiteByName(1,'zap')}, undef,'getTestSuiteByName with 2 args returns no error');
 is(exception {$tr->createCase(1,'whee')}, undef,'createCase with 2 args returns no error');
 is(exception {$tr->createCase(1,'whee')}, undef,'createCase with 2 args returns no error');
+is(exception {$tr->getChildRunByName({},'whee')},undef,'getChildRunByName returns no error when 2 arguments passed');
 
 
 isnt(exception {$tr->createRun(1,1)}, undef,'createRun with 2 args returns error');
 isnt(exception {$tr->createRun(1,1)}, undef,'createRun with 2 args returns error');
 isnt(exception {$tr->createSection(1,1)}, undef,'createSection with 2 args returns error');
 isnt(exception {$tr->createSection(1,1)}, undef,'createSection with 2 args returns error');