Explorar el Código

Fix #21 and #22

Add ability to spawn cases, etc.
George S. Baugh hace 10 años
padre
commit
594104683d

+ 8 - 0
Changes

@@ -1,5 +1,13 @@
 Revision history for Perl module TestRail::API
 
+0.019 2015-03-18 TEODESIAN
+    - Add createRunInPlan method to TestRail::API
+    - Add translateConfigNamesToIds method to TestRail::API
+    - Modified getConfigurations, and added GetConfigurationGroups for clarity
+    - Add ability to spawn runs to App::Prove::TestRail and testrail-report
+    - Stricter checking that passed configurations passed exist in Test::Rail::Parser
+    - Require minimum version of JSON::Maybe::XS to resolve smoker failures.
+
 0.018 2015-01-29 TEODESIAN
     - Better finding of $HOME in testrail-report and the prove plugin for cross-platform usage
     - Track elapsed time of tests when run as prove plugin, and report this to testrail

+ 23 - 3
bin/testrail-report

@@ -40,7 +40,13 @@ You can do the same outside of plans, and without configurations; but doing so i
 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.
+This should provide sufficient uniqueness to get any run using names.
+
+=head3 OPTIONAL PARAMETERS
+
+    --spawn [testsuite_id] : Attempt to create a run based on the provided testsuite ID.  Uses the name provided with --run.
+      If plans/configurations are supplied, it will attempt to create it as a child of the provided plan, and with the supplied configurations.
+      If the specified run already exists, the program will simply use the existing run, and disregard the supplied testsuite_id.
 
 =head3 CONFIG OVERRIDES
 
@@ -145,6 +151,19 @@ PARAMETERS:
   you want to set results for.  This should provide sufficient
   uniqueness to get to any run using words.
 
+  [OPTIONAL PARAMETERS]
+
+    --spawn [testsuite_id] : Attempt to create a run based on the
+      provided testsuite ID.  Uses the name provided with --run.
+
+      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.
+
   [CONFIG OVERRIDES]
   In your \$HOME, (or the current directory, if your system has no
   concept of a home directory) put a file called .testrailrc with
@@ -221,7 +240,7 @@ sub parseConfig {
 
 #Main loop------------
 
-my ($help,$apiurl,$user,$password,$project,$run,$case_per_ok,$step_results,$mock,$configs,$plan,$version);
+my ($help,$apiurl,$user,$password,$project,$run,$case_per_ok,$step_results,$mock,$configs,$plan,$version,$spawn);
 
 #parse switches
 GetOptions(
@@ -236,6 +255,7 @@ GetOptions(
     'config=s@'      => \$configs,
     'plan=s'         => \$plan,
     'version=s'      => \$version,
+    'spawn=i'        => \$spawn,
     'help'           => \$help
 );
 
@@ -351,10 +371,10 @@ foreach my $phil (@files) {
         'step_results' => $step_results,
         'debug'        => $debug,
         'browser'      => $browser,
-        'version'      => $version,
         'plan'         => $plan,
         'configs'      => $configs,
         'result_options' => $result_options,
+        'spawn'        => $spawn,
         'merge'        => 1
     });
     $tap->run();

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

@@ -34,11 +34,14 @@ If \$HOME/.testrailrc exists, it will be parsed for any of these values in a new
     version=xx.xx.xx.xx
     case_per_ok=0
     step_results=sr_sys_name
+    spawn=123
 
 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 \$HOME/.testrailrc.
 If your system has no concept of user homes, it will look in the current directory for .testrailrc.
 
+See the documentation for the constructor of L<Test::Rail::Parser> as to why you might want to pass the aforementioned options.
+
 =head1 OVERRIDDEN METHODS
 
 =head2 load(parser)
@@ -82,6 +85,7 @@ sub load {
     $ENV{'TESTRAIL_VERSION'} = $params->{version};
     $ENV{'TESTRAIL_CASEOK'}  = $params->{case_per_ok};
     $ENV{'TESTRAIL_STEPS'}   = $params->{step_results};
+    $ENV{'TESTRAIL_SPAWN'}   = $params->{spawn};
     return $class;
 }
 

+ 231 - 3
lib/Test/LWP/UserAgent/TestRailMock.pm

@@ -554,6 +554,33 @@ $mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4,
 
 {
 
+$VAR1 = 'index.php?/api/v2/add_run/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:09 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:09 GMT',
+                 'content-type' => 'application/json; charset=utf-8',
+                 'server' => 'Apache/2.4.7 (Ubuntu)'
+               }, 'HTTP::Headers' );
+$VAR5 = '{"id":8675309,"suite_id":9,"name":"TestingSuite2","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_runs/9';
 $VAR2 = '200';
 $VAR3 = 'OK';
@@ -876,7 +903,8 @@ $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"}]';
+$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"},
+{"id":24,"name":"mah dubz plan","description":"bogozone","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\\/24"}]';
 $mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
 
 }
@@ -908,6 +936,34 @@ $mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4,
 
 }
 
+{
+
+$VAR1 = 'index.php?/api/v2/get_plan/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' => '1289',
+                 '::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":24,"name":"mah dubz plan","description":"bogoplan","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\\/24","entries":[{"id":"271443a5-aacf-467e-8993-b4f7001195cf","suite_id":9,"name":"Executing the great plan","runs":[{"id":1,"suite_id":9,"name":"TestingSuite","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":[2],"url":"http:\\/\\/testrail.local\\/\\/index.php?\\/runs\\/view\\/24"}]}]}';
+$mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
+
+}
+
+
 {
 
 $VAR1 = 'index.php?/api/v2/get_tests/22';
@@ -992,6 +1048,33 @@ $mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4,
 
 {
 
+$VAR1 = 'index.php?/api/v2/get_tests/8675309';
+$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:11 GMT',
+                 'content-type' => 'application/json; charset=utf-8',
+                 'server' => 'Apache/2.4.7 (Ubuntu)'
+               }, 'HTTP::Headers' );
+$VAR5 = '[{"id":15,"case_id":8,"status_id":3,"assignedto_id":null,"run_id":22,"title":"STORAGE TANKS SEARED","type_id":6,"priority_id":4,"estimate":null,"estimate_forecast":null,"refs":null,"milestone_id":null,"custom_preconds":null,"custom_steps":null,"custom_expected":null},{"id":15,"case_id":8,"status_id":3,"assignedto_id":null,"run_id":22,"title":"NOT SO SEARED AFTER ARR"},{"id":15,"case_id":8,"status_id":3,"assignedto_id":null,"run_id":22,"title":"skipall.test"} ]';
+$mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
+
+}
+
+{
+
 $VAR1 = 'index.php?/api/v2/get_test/15';
 $VAR2 = '200';
 $VAR3 = 'OK';
@@ -1419,7 +1502,53 @@ $VAR4 = bless( {
                  'content-type' => 'application/json; charset=utf-8',
                  'server' => 'Apache/2.4.7 (Ubuntu)'
                }, 'HTTP::Headers' );
-$VAR5 = '[]';
+#Ripped from the headlines, lol -- see TR documentation
+$VAR5 = '[
+    {
+        "configs": [
+            {
+                "group_id": 1,
+                "id": 1,
+                "name": "Chrome"
+            },
+            {
+                "group_id": 1,
+                "id": 2,
+                "name": "Firefox"
+            },
+            {
+                "group_id": 1,
+                "id": 3,
+                "name": "Internet Explorer"
+            }
+        ],
+        "id": 1,
+        "name": "Browsers",
+        "project_id": 1
+    },
+    {
+        "configs": [
+            {
+                "group_id": 2,
+                "id": 6,
+                "name": "Ubuntu 12"
+            },
+            {
+                "group_id": 2,
+                "id": 4,
+                "name": "Windows 7"
+            },
+            {
+                "group_id": 2,
+                "id": 5,
+                "name": "Windows 8"
+            }
+        ],
+        "id": 2,
+        "name": "Operating Systems",
+        "project_id": 1
+    }
+]';
 $mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
 
 }
@@ -1446,7 +1575,106 @@ $VAR4 = bless( {
                  'content-type' => 'application/json; charset=utf-8',
                  'server' => 'Apache/2.4.7 (Ubuntu)'
                }, 'HTTP::Headers' );
-$VAR5 = '["testConfig"]';
+$VAR5 = '[{
+        "configs": [
+            {
+                "group_id": 1,
+                "id": 1,
+                "name": "testConfig"
+            },
+            {
+                "group_id": 1,
+                "id": 2,
+                "name": "testPlatform1"
+            }
+        ],
+        "id": 1,
+        "name": "Bullpucky",
+        "project_id": 1
+    }
+]';
+$mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
+
+}
+
+{
+
+$VAR1 = 'index.php?/api/v2/add_plan_entry/23';
+$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 = '{"runs": [{"id":666}]}';
+$mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
+
+}
+
+{
+
+$VAR1 = 'index.php?/api/v2/add_plan_entry/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: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 = '{"runs": [{"id":8675309}]}';
+$mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
+
+}
+
+
+{
+
+$VAR1 = 'index.php?/api/v2/get_run/666';
+$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":666,"suite_id":9,"name":"Dynamic Plan Run","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":23,"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));
 
 }

+ 1 - 0
lib/Test/Rail/Harness.pm

@@ -53,6 +53,7 @@ sub make_parser {
     $args->{'result_options'} = {'version' => $ENV{'TESTRAIL_VERSION'}} if $ENV{'TESTRAIL_VERSION'};
     $args->{'case_per_ok'}    = $ENV{'TESTRAIL_CASEOK'};
     $args->{'step_results'}   = $ENV{'TESTRAIL_STEPS'};
+    $args->{'spawn'}          = $ENV{'TESTRAIL_SPAWN'};
 
     #for Testability of plugin
     if ($ENV{'TESTRAIL_MOCKED'}) {

+ 33 - 5
lib/Test/Rail/Parser.pm

@@ -72,6 +72,8 @@ Get the TAP Parser ready to talk to TestRail, and register a bunch of callbacks
 
 =item B<custom_options> - HASHREF (optional): Custom options to set with your result.  See L<TestRail::API>'s createTestResults function for more information.  step_results will be set here, if the option is passed.
 
+=item B<spawn> - INTEGER (optional): Attempt to create a run based on the provided testsuite identified by the ID passed here.  If plan/configs is passed, create it as a child of said plan with the listed configs.  If the run exists, use it and disregard the provided testsuite ID.
+
 =back
 
 =back
@@ -108,7 +110,8 @@ sub new {
         'step_results' => delete $opts->{'step_results'},
         'case_per_ok'  => delete $opts->{'case_per_ok'},
         'plan'         => delete $opts->{'plan'},
-        'configs'      => delete $opts->{'configs'},
+        'configs'      => delete $opts->{'configs'} // [],
+        'spawn'        => delete $opts->{'spawn'},
         #Stubs for extension by subclassers
         'result_options'        => delete $opts->{'result_options'},
         'result_custom_options' => delete $opts->{'result_custom_options'}
@@ -154,16 +157,21 @@ sub new {
 
     #Grab run
     my $run_id = $tropts->{'run_id'};
-    my $run;
+    my ($run,$plan,$config_ids);
 
-    #TODO check if configs passed are defined for project
+    #check if configs passed are defined for project.  If we can't get all the IDs, something's hinky
+    $config_ids = $tr->translateConfigNamesToIds($tropts->{'project_id'},$tropts->{'configs'});
+    confess("Could not retrieve list of valid configurations for your project.") unless (reftype($config_ids) || 'undef') eq 'ARRAY';
+    my @bogus_configs = grep {!defined($_)} @$config_ids;
+    my $num_bogus = scalar(@bogus_configs);
+    confess("Detected $num_bogus bad config names passed.  Check available configurations for your project.") if $num_bogus;
 
     if ($tropts->{'run'}) {
         if ($tropts->{'plan'}) {
             #Attempt to find run, filtered by configurations
-            my $plan = $tr->getPlanByName($tropts->{'project_id'},$tropts->{'plan'});
+            $plan = $tr->getPlanByName($tropts->{'project_id'},$tropts->{'plan'});
             if ($plan) {
-                $tropts->{'plan'} = $plan; #XXX Save for later just in case?
+                $tropts->{'plan'} = $plan;
                 $run = $tr->getChildRunByName($plan,$tropts->{'run'},$tropts->{'configs'}); #Find plan filtered by configs
                 if (defined($run) && (reftype($run) || 'undef') eq 'HASH') {
                     $tropts->{'run'} = $run;
@@ -182,6 +190,26 @@ sub new {
     } else {
         $tropts->{'run'} = $tr->getRunByID($run_id);
     }
+
+    #If spawn was passed and we don't have a Run ID yet, go ahead and make it
+    if ($tropts->{'spawn'} && !$tropts->{'run_id'}) {
+        if ($tropts->{'plan'}) {
+            $plan = $tr->createRunInPlan( $tropts->{'plan'}->{'id'}, $tropts->{'spawn'}, $tropts->{'run'}, undef, $config_ids );
+            $run = $plan->{'runs'}->[0] if exists($plan->{'runs'}) && (reftype($plan->{'runs'}) || 'undef') eq 'ARRAY' && scalar(@{$plan->{'runs'}});
+            if (defined($run) && (reftype($run) || 'undef') eq 'HASH') {
+                $tropts->{'run'} = $run;
+                $tropts->{'run_id'} = $run->{'id'};
+            }
+        } else {
+            $run = $tr->createRun( $tropts->{'project_id'}, $tropts->{'spawn'}, $tropts->{'run'}, "Automatically created Run from TestRail::API" );
+            if (defined($run) && (reftype($run) || 'undef') eq 'HASH') {
+                $tropts->{'run'} = $run;
+                $tropts->{'run_id'} = $run->{'id'};
+            }
+        }
+        confess("Could not spawn run with requested parameters!") 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);

+ 114 - 7
lib/TestRail/API.pm

@@ -83,7 +83,7 @@ sub new {
         flattree         => [],
         user_cache       => [],
         type_cache       => [],
-        configurations   => undef,
+        configurations   => {},
         tr_fields        => undef,
         default_request  => undef,
         browser          => new LWP::UserAgent()
@@ -157,7 +157,6 @@ sub _doRequest {
     my $response = $self->{'browser'}->request($req);
 
     return $response if !defined($response); #worst case
-
     if ($response->code == 403) {
         cluck "ERROR: Access Denied.";
         return -403;
@@ -1338,6 +1337,56 @@ sub getPlanByID {
     return $self->_doRequest("index.php?/api/v2/get_plan/$plan_id");
 }
 
+=head2 B<createRunInPlan (plan_id,suite_id,name,description,milestone_id,assigned_to_id,case_ids)>
+
+Create a run.
+
+=over 4
+
+=item INTEGER C<PLAN ID> - ID of parent project.
+
+=item INTEGER C<SUITE ID> - ID of suite to base run on
+
+=item STRING C<NAME> - Name of run
+
+=item INTEGER C<ASSIGNED TO ID> (optional) - User to assign the run to
+
+=item ARRAYREF C<CONFIG IDS> (optional) - Array of Configuration IDs (see getConfigurations) to apply to the created run
+
+=item ARRAYREF C<CASE IDS> (optional) - Array of case IDs in case you don't want to use the whole testsuite when making the build.
+
+=back
+
+Returns run definition HASHREF.
+
+    $tr->createRun(1,1345,'PlannedRun',3,[1,4,77],[3,4,5,6]);
+
+=cut
+
+#If you pass an array of case ids, it implies include_all is false
+sub createRunInPlan {
+    my ($self,$plan_id,$suite_id,$name,$assignedto_id,$config_ids,$case_ids) = @_;
+    confess("Object methods must be called by an instance") unless ref($self);
+    confess("Plan ID must be integer") unless $self->_checkInteger($plan_id);
+    confess("Suite ID must be integer") unless $self->_checkInteger($suite_id);
+    confess("Name must be string") unless $self->_checkString($name);
+    confess("Assigned To ID must be integer") unless !defined($assignedto_id) || $self->_checkInteger($assignedto_id);
+    confess("Config IDs must be ARRAYREF") unless !defined($config_ids) || (reftype($config_ids) || 'undef') eq 'ARRAY';
+    confess("Case IDs must be ARRAYREF") unless !defined($case_ids) || (reftype($case_ids) || 'undef') eq 'ARRAY';
+
+    my $stuff = {
+        suite_id      => $suite_id,
+        name          => $name,
+        assignedto_id => $assignedto_id,
+        include_all   => defined($case_ids) ? 0 : 1,
+        case_ids      => $case_ids,
+        config_ids    => $config_ids
+    };
+
+    my $result = $self->_doRequest("index.php?/api/v2/add_plan_entry/$plan_id",'POST',$stuff);
+    return $result;
+}
+
 =head1 MILESTONE METHODS
 
 =head2 B<createMilestone (project_id,name,description,due_on)>
@@ -1730,9 +1779,35 @@ sub getTestResults {
 
 =head1 CONFIGURATION METHODS
 
+=head2 B<getConfigurationGroups(project_id)>
+
+Gets the available configuration groups for a project, with their configurations as children.
+Basically a direct wrapper of The 'get_configs' api call, with caching tacked on.
+
+=over 4
+
+=item INTEGER C<PROJECT_ID> - ID of relevant project
+
+=back
+
+Returns ARRAYREF of configuration group definition HASHREFs.
+
+=cut
+
+sub getConfigurationGroups {
+    my ($self,$project_id) = @_;
+    confess("Object methods must be called by an instance") unless ref($self);
+    confess("Project ID must be positive integer") unless $self->_checkInteger($project_id);
+    my $url = "index.php?/api/v2/get_configs/$project_id";
+    return $self->{'configurations'}->{$project_id} if $self->{'configurations'}->{$project_id}; #cache this since we can't change it with the API
+    $self->{'configurations'}->{$project_id} = $self->_doRequest($url);
+    return $self->{'configurations'}->{$project_id};
+}
+
 =head2 B<getConfigurations(project_id)>
 
 Gets the available configurations for a project.
+Mostly for convenience (no need to write a boilerplate loop over the groups).
 
 =over 4
 
@@ -1741,17 +1816,49 @@ Gets the available configurations for a project.
 =back
 
 Returns ARRAYREF of configuration definition HASHREFs.
+Returns result of getConfigurationGroups (likely -500) in the event that call fails.
 
 =cut
 
 sub getConfigurations {
     my ($self,$project_id) = @_;
     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->{'configurations'} if $self->{'configurations'}; #cache this since we can't change it with the API
-    $self->{'configurations'} = $self->_doRequest($url);
-    return $self->{'configurations'};
+    confess("Project ID must be positive integer") unless $self->_checkInteger($project_id);
+    my $cgroups = $self->getConfigurationGroups($project_id);
+    my $configs = [];
+    return $cgroups unless (reftype($cgroups) || 'undef') eq 'ARRAY';
+    foreach my $cfg (@$cgroups) {
+        push(@$configs, @{$cfg->{'configs'}});
+    }
+    return $configs;
+}
+
+=head2 B<translateConfigNamesToIds(project_id,configs)>
+
+Transforms a list of configuration names into a list of config IDs.
+
+=over 4
+
+=item INTEGER C<PROJECT_ID> - Relevant project ID for configs.
+
+=item ARRAYREF C<CONFIGS> - Array ref of config names
+
+=back
+
+Returns ARRAYREF of configuration names, with undef values for unknown configuration names.
+
+=cut
+
+sub translateConfigNamesToIds {
+    my ($self,$project_id,$configs) = @_;
+    confess("Object methods must be called by an instance") unless ref($self);
+    confess("Project ID must be positive integer") unless $self->_checkInteger($project_id);
+    confess("Configs must be arrayref") unless (reftype($configs) || 'undef') eq 'ARRAY';
+    return [] if !scalar(@$configs);
+    my $existing_configs = $self->getConfigurations($project_id);
+    return map {undef} @$configs if (reftype($existing_configs) || 'undef') ne 'ARRAY';
+    my @ret = map {my $name = $_; my @candidates = grep { $name eq $_->{'name'} } @$existing_configs; scalar(@candidates) ? $candidates[0]->{'id'} : undef } @$configs;
+    return \@ret;
 }
 
 =head1 STATIC METHODS

+ 8 - 1
t/App-Prove-Plugin-Testrail.t

@@ -3,7 +3,7 @@
 use strict;
 use warnings;
 
-use Test::More 'tests' => 2;
+use Test::More 'tests' => 3;
 use Test::Fatal;
 use App::Prove;
 use App::Prove::Plugin::TestRail;
@@ -22,3 +22,10 @@ $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 {$prove->run()},undef,"Running TR parser case via plugin functions works with configs/plans");
+
+#Check that spawn options make it through
+
+$prove = App::Prove->new();
+$prove->process_args("-PTestRail=apiurl=http://some.testlink.install/,user=someUser,password=somePassword,project=TestProject,run=TestingSuite2,version=0.014,case_per_ok=1,spawn=9",'t/skipall.test');
+
+is (exception {$prove->run()},undef,"Running TR parser case via plugin functions works with configs/plans");

+ 97 - 1
t/Test-Rail-Parser.t

@@ -7,7 +7,7 @@ use Scalar::Util qw{reftype};
 use TestRail::API;
 use Test::LWP::UserAgent::TestRailMock;
 use Test::Rail::Parser;
-use Test::More 'tests' => 26;
+use Test::More 'tests' => 36;
 use Test::Fatal qw{exception};
 
 #Same song and dance as in TestRail-API.t
@@ -225,4 +225,100 @@ if (!$res) {
     is($tap->{'errors'},0,"No errors encountered uploading case results");
 }
 
+#Ok, let's test the plan, config, and spawn bits.
+undef $tap;
+$res = exception {
+    $tap = Test::Rail::Parser->new({
+        'source'              => 't/skipall.test',
+        'apiurl'              => $apiurl,
+        'user'                => $login,
+        'pass'                => $pw,
+        'debug'               => $debug,
+        'browser'             => $browser,
+        'run'                 => 'hoo hoo I do not exist',
+        'plan'                => 'mah dubz plan',
+        'configs'             => ['testPlatform1'],
+        'project'             => 'TestProject',
+        'merge'               => 1
+    });
+};
+isnt($res,undef,"TR Parser explodes on instantiation when asking for run not in plan");
+
+undef $tap;
+$res = exception {
+    $tap = Test::Rail::Parser->new({
+        'source'              => 't/skipall.test',
+        'apiurl'              => $apiurl,
+        'user'                => $login,
+        'pass'                => $pw,
+        'debug'               => $debug,
+        'browser'             => $browser,
+        'run'                 => 'TestingSuite',
+        'plan'                => 'mah dubz plan',
+        'configs'             => ['testPlatform1'],
+        'project'             => 'TestProject',
+        'merge'               => 1
+    });
+};
+is($res,undef,"TR Parser doesn't explode on instantiation looking for existing run in plan");
+isa_ok($tap,"Test::Rail::Parser");
+
+if (!$res) {
+    $tap->run();
+    is($tap->{'errors'},0,"No errors encountered uploading case results");
+}
+
+#Now, test spawning.
+undef $tap;
+$res = exception {
+    $tap = Test::Rail::Parser->new({
+        'source'              => 't/skipall.test',
+        'apiurl'              => $apiurl,
+        'user'                => $login,
+        'pass'                => $pw,
+        'debug'               => $debug,
+        'browser'             => $browser,
+        'run'                 => 'TestingSuite2',
+        'plan'                => 'mah dubz plan',
+        'configs'             => ['testPlatform1'],
+        'project'             => 'TestProject',
+        'spawn'               => 9,
+        'merge'               => 1
+    });
+};
+is($res,undef,"TR Parser doesn't explode on instantiation when spawning run in plan");
+isa_ok($tap,"Test::Rail::Parser");
+
+if (!$res) {
+    $tap->run();
+    is($tap->{'errors'},0,"No errors encountered uploading case results");
+}
+
+#Test spawning of builds not in plans.
+#Now, test spawning.
+undef $tap;
+$res = exception {
+    $tap = Test::Rail::Parser->new({
+        'source'              => 't/skipall.test',
+        'apiurl'              => $apiurl,
+        'user'                => $login,
+        'pass'                => $pw,
+        'debug'               => $debug,
+        'browser'             => $browser,
+        'run'                 => 'TestingSuite2',
+        'project'             => 'TestProject',
+        'spawn'               => 9,
+        'merge'               => 1
+    });
+};
+is($res,undef,"TR Parser doesn't explode on instantiation when spawning run in plan");
+isa_ok($tap,"Test::Rail::Parser");
+
+if (!$res) {
+    $tap->run();
+    is($tap->{'errors'},0,"No errors encountered uploading case results");
+}
+
+
+
 0;

+ 16 - 7
t/TestRail-API.t

@@ -4,7 +4,7 @@ use warnings;
 use TestRail::API;
 use Test::LWP::UserAgent::TestRailMock;
 
-use Test::More tests => 55;
+use Test::More tests => 57;
 use Test::Fatal;
 use Scalar::Util 'reftype';
 use ExtUtils::MakeMaker qw{prompt};
@@ -16,11 +16,6 @@ my $pw     = $ENV{'TESTRAIL_PASSWORD'};
 #Mock if nothing is provided
 my $is_mock = (!$apiurl && !$login && !$pw);
 
-#EXAMPLE:
-#my $apiurl = 'http://testrails.cpanel.qa/testrail';
-#my $login = 'some.guyb@whee.net';
-#my $pw = '5gP77MdrSIB68UFWvhIK';
-
 like(exception {TestRail::API->new('trash');}, qr/invalid uri/i, "Non-URIs bounce constructor");
 like(exception {TestRail::API->new('http://hokum.bogus','lies','moreLies',0); }, qr/Could not communicate with TestRail Server/i,"Bogus Testrail URI rejected");
 
@@ -121,6 +116,11 @@ is($tr->getRunByID($prun->{'id'})->{'name'},"Executing the great plan","Can get
 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")->{'id'},undef,"Getting run by name returns child runs");
 
+#Test createRunInPlan
+my $updatedPlan = $tr->createRunInPlan($new_plan->{'id'},$new_suite->{'id'},'Dynamic Plan Run');
+$prun = $updatedPlan->{'runs'}->[0];
+is($tr->getRunByID($prun->{'id'})->{'name'},"Dynamic Plan Run","Can get newly created child run of plan by ID");
+
 #Test TEST/RESULT methods
 my $tests = $tr->getTests($new_run->{'id'});
 ok($tests,"Can get tests");
@@ -140,7 +140,16 @@ is($results->[0]->{'id'},$result->{'id'},"Can get results for test");
 
 #Test configuration methods
 my $configs = $tr->getConfigurations($new_project->{'id'});
-is(reftype($configs),'ARRAY',"Can get configurations for a project");
+my $is_arr = is(reftype($configs),'ARRAY',"Can get configurations for a project");
+my (@config_names,@config_ids);
+if ($is_arr) {
+    @config_names = map {$_->{'name'}} @$configs;
+    @config_ids = map {$_->{'id'}} @$configs;
+}
+my $t_config_ids = $tr->translateConfigNamesToIds($new_project->{'id'},\@config_names);
+@config_ids = sort(@config_ids);
+@$t_config_ids = sort(@$t_config_ids);
+is_deeply(\@config_ids,$t_config_ids, "Can correctly translate Project names to IDs");
 
 #Delete a plan
 ok($tr->deletePlan($new_plan->{'id'}),"Can delete plan");

+ 11 - 2
t/arg_types.t

@@ -2,7 +2,7 @@ use strict;
 use warnings;
 
 use TestRail::API;
-use Test::More 'tests' => 120;
+use Test::More 'tests' => 129;
 use Test::Fatal;
 use Class::Inspector;
 use Test::LWP::UserAgent;
@@ -62,9 +62,12 @@ isnt( exception {$tr->getSections() },undef,'getSections returns error when no a
 isnt( exception {$tr->getRuns() },undef,'getRuns 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->getConfigurations() },undef,'getConfigurations returns error when no arguments are passed');
+isnt( exception {$tr->getConfigurationGroups() },undef,'getConfigurations returns error when no arguments are passed');
+isnt( exception {$tr->getConfigurations() },undef,'getConfigurationGroups 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');
+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');
 
 #1-arg functions
 is(exception {$tr->deleteCase(1)},            undef,'deleteCase returns no error when int arg passed');
@@ -94,6 +97,7 @@ is(exception {$tr->getCaseTypeByName('zap')}, undef,'getCaseTypeByName returns n
 is(exception {$tr->createProject('zippy')},   undef,'createProject returns no error when string arg passed');
 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->getConfigurationGroups(1)},undef,'getConfigurationGroups 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');
 
@@ -114,6 +118,8 @@ isnt(exception {$tr->getSections(1)}, undef,'getSections with 1 arg returns erro
 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');
+isnt( exception {$tr->createRunInPlan(1) },undef,'createRunInPlan returns error when 1 argument passed');
+isnt( exception {$tr->translateConfigNamesToIds(1)}, undef,'translateConfigNamesToIds returns error when 1 argument passed');
 
 #2 arg functions
 is(exception {$tr->createMilestone(1,'whee')}, undef,'createMilestone with 2 args returns no error');
@@ -128,18 +134,21 @@ is(exception {$tr->getTestByName(1,'poo')}, undef,'getTestByName with 2 args ret
 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');
+is(exception {$tr->translateConfigNamesToIds(1,[1,2,3])}, undef,'translateConfigNamesToIds 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');
 isnt(exception {$tr->getCaseByName(1,1)}, undef,'getCaseByName with 2 args returns error');
 isnt(exception {$tr->getCases(1,2)}, undef,'getCases with 2 args returns error');
 isnt(exception {$tr->getSectionByName(1,1)}, undef,'getSectionByName with 2 args returns error');
+isnt( exception {$tr->createRunInPlan(1,1) },undef,'createRunInPlan returns error when 2 arguments passed');
 
 #3 arg functions
 is(exception {$tr->createRun(1,1,'whee')}, undef,'createRun with 3 args returns no error');
 is(exception {$tr->createSection(1,1,'whee')}, undef,'createSection with 3 args returns no error');
 is(exception {$tr->getCases(1,2,3)}, undef,'getCases with 3 args returns no error');
 is(exception {$tr->getSectionByName(1,1,'zip')}, undef,'getSectionByName with 3 args returns no error');
+is( exception {$tr->createRunInPlan(1,1,'nugs') },undef,'createRunInPlan with 3 args returns no error');
 
 isnt(exception {$tr->getCaseByName(1,1,1)}, undef,'getCaseByName with 3 args returns error');
 

+ 15 - 2
t/testrail-report.t

@@ -1,7 +1,7 @@
 use strict;
 use warnings;
 
-use Test::More 'tests' => 6;
+use Test::More 'tests' => 10;
 
 my @args = ($^X,qw{bin/testrail-report --apiurl http://testrail.local --user "test@fake.fake" --password "fake" --project "CRUSH ALL HUMANS" --run "SEND T-1000 INFILTRATION UNITS BACK IN TIME" --mock t/test_multiple_files.tap});
 my $out = `@args`;
@@ -15,10 +15,23 @@ is($? >> 8, 0, "Exit code OK reported with multiple files (case-ok mode)");
 $matches = () = $out =~ m/Reporting result of case/ig;
 is($matches,4,"Attempts to upload multiple times (case-ok mode)");
 
-@args = ($^X,qw{bin/testrail-report --apiurl http://testrail.local --user "test@fake.fake" --password "fake" --project "TestProject" --run "TestingSuite" --case-ok --mock t/test_subtest.tap});
+#Test version, case-ok
+@args = ($^X,qw{bin/testrail-report --apiurl http://testrail.local --user "test@fake.fake" --password "fake" --project "TestProject" --run "TestingSuite" --case-ok --version '1.0.14' --mock t/test_subtest.tap});
 $out = `@args`;
 is($? >> 8, 0, "Exit code OK reported with subtests (case-ok mode)");
 $matches = () = $out =~ m/Reporting result of case/ig;
 is($matches,2,"Attempts to upload do not do subtests (case-ok mode)");
 
+#Test plans/configs
+@args = ($^X,qw{bin/testrail-report --apiurl http://testrail.local --user "test@fake.fake" --password "fake" --project "TestProject" --run "Executing the great plan" --plan "GosPlan" --config "testConfig"  --case-ok --mock t/test_subtest.tap});
+$out = `@args`;
+is($? >> 8, 0, "Exit code OK reported with plans");
+$matches = () = $out =~ m/Reporting result of case.*OK/ig;
+is($matches,2,"Attempts to to plans work");
 
+#Test that spawn works
+@args = ($^X,qw{bin/testrail-report --apiurl http://testrail.local --user "test@fake.fake" --password "fake" --project "TestProject" --run "TestingSuite2" --spawn 9 --case-ok --mock t/test_subtest.tap});
+$out = `@args`;
+is($? >> 8, 0, "Exit code OK reported with spawn");
+$matches = () = $out =~ m/Reporting result of case.*OK/ig;
+is($matches,2,"Attempts to spawn work");