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
     - Set SKIP_ALL tests status correctly
     - 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
     - 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
 
 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/";
 > 
@@ -23,4 +22,8 @@ TODO: alter tests so that you can keep in sync.
 > 
 > 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.

+ 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
 (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
 
-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
 
 =head3 CONFIG OPTIONS
@@ -45,13 +59,17 @@ These override the config, if present.  If neither are used, you will be prompte
 
 =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.
 
 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.
 
+=head3 RESULT OPTIONS
+
+    --version : String describing the version of the system under test.
+
 =head2 PROVE PLUGIN:
 
 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
   (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]
   In your \$HOME, put a file called .testrailrc with key=value
   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
   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:
 
   passing -PTestRail=apiurl,user,pass,project,run to prove will
@@ -176,7 +217,7 @@ sub parseConfig {
 
 #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
 GetOptions(
@@ -188,6 +229,9 @@ GetOptions(
     'case-ok'        => \$case_per_ok,
     'step-results=s' => \$step_results,
     'mock'           => \$mock,
+    'config=s@'      => \$configs,
+    'plan=s'         => \$plan,
+    'version=s'      => \$version,
     'help'           => \$help
 );
 
@@ -285,21 +329,28 @@ if ($mock) {
     $debug = 1;
 }
 
+my $result_options = undef;
+$result_options = {'version' => $version} if $version;
+
 my $tap;
 foreach my $phil (@files) {
     print "Processing $phil...\n";
     $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();
 

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

@@ -9,7 +9,7 @@ use utf8;
 
 =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
 
@@ -17,7 +17,8 @@ use utf8;
 
 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:
 
@@ -26,10 +27,14 @@ If ~/.testrailrc exists, it will be parsed for any of these values in a newline
     password=superS3cret
     project=TestProject
     run=TestRun
+    plan=GosPlan
+    configs=config1:config2:config3: ... :configN
+    version=xx.xx.xx.xx
     case_per_ok=0
     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
 
@@ -47,27 +52,35 @@ sub load {
 
     my $app = $p->{app_prove};
     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->merge(1);
 
     #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;
 }
 

+ 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';
@@ -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';
 $VAR3 = 'OK';
 $VAR4 = bless( {
@@ -875,7 +903,7 @@ $VAR4 = bless( {
                  'content-type' => 'application/json; charset=utf-8',
                  'server' => 'Apache/2.4.7 (Ubuntu)'
                }, '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));
 
 }
@@ -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;

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

@@ -46,8 +46,19 @@ sub make_parser {
     $args->{'pass'}    = $ENV{'TESTRAIL_PASS'};
     $args->{'project'} = $ENV{'TESTRAIL_PROJ'};
     $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 );
     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'},
         'step_results' => delete $opts->{'step_results'},
         'case_per_ok'  => delete $opts->{'case_per_ok'},
+        'plan'         => delete $opts->{'plan'},
+        'configs'      => delete $opts->{'configs'},
         #Stubs for extension by subclassers
         'result_options'        => delete $opts->{'result_options'},
         'result_custom_options' => delete $opts->{'result_custom_options'}
@@ -143,18 +145,37 @@ sub new {
     $tropts->{'todo_fail'} = $todof[0];
     $tropts->{'todo_pass'} = $todop[0];
 
-    #Grab suite from run
+    #Grab run
     my $run_id = $tropts->{'run_id'};
+    my $run;
+
+    #TODO check if configs passed are defined for project
+
     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 {
         $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);
     if (defined($self->{'_iterator'}->{'command'}) && reftype($self->{'_iterator'}->{'command'}) eq 'ARRAY' ) {

+ 82 - 5
lib/TestRail/API.pm

@@ -82,6 +82,7 @@ sub new {
         flattree         => [],
         user_cache       => [],
         type_cache       => [],
+        configurations   => undef,
         tr_fields        => undef,
         default_request  => undef,
         browser          => new LWP::UserAgent()
@@ -1109,6 +1110,75 @@ sub getRunByID {
     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
 
 =head2 B<createPlan (project_id,name,description,milestone_id,entries)>
@@ -1185,9 +1255,9 @@ sub deletePlan {
     return $result;
 }
 
-=head2 B<getPlans (project_id)>
+=head2 B<getPlans (project_id,get_runs)>
 
-Deletes specified plan.
+Gets specified test plans.
 
 =over 4
 
@@ -1199,6 +1269,9 @@ Returns ARRAYREF of plan definition HASHREFs.
 
     $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
 
 sub getPlans {
@@ -1234,7 +1307,9 @@ sub getPlanByName {
     my $plans = $self->getPlans($project_id);
     return -500 if !$plans || (reftype($plans) || 'undef') ne 'ARRAY';
     foreach my $plan (@$plans) {
-        return $plan if $plan->{'name'} eq $name;
+        if ($plan->{'name'} eq $name) {
+            return $self->getPlanByID($plan->{'id'});
+        }
     }
     return 0;
 }
@@ -1622,7 +1697,7 @@ sub createTestResults {
     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.
 
@@ -1673,7 +1748,9 @@ sub getConfigurations {
     confess("Object methods must be called by an instance") unless ref($self);
     confess("Test ID must be positive integer") unless $self->_checkInteger($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

+ 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 Test::LWP::UserAgent::TestRailMock;
 
-use Test::More tests => 52;
+use Test::More tests => 55;
 use Test::Fatal;
 use Scalar::Util 'reftype';
 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");
 
 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");
 
+#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
 my $tests = $tr->getTests($new_run->{'id'});
 ok($tests,"Can get tests");
@@ -125,6 +132,7 @@ my $statusTypes = $tr->getPossibleTestStatuses();
 ok($resTypes,"Can get test result fields");
 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");
 ok(defined($result->{'id'}),"Can add test results");
 my $results = $tr->getTestResults($tests->[0]->{'id'});

+ 6 - 1
t/arg_types.t

@@ -2,7 +2,7 @@ use strict;
 use warnings;
 
 use TestRail::API;
-use Test::More 'tests' => 115;
+use Test::More 'tests' => 120;
 use Test::Fatal;
 use Class::Inspector;
 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->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->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
 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->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->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->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->getTestByName(1)}, undef,'getTestByName 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
 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->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->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->createSection(1,1)}, undef,'createSection with 2 args returns error');