Эх сурвалжийг харах

Fix #37: Add ability to discriminate by section name to TR parser

George S. Baugh 10 жил өмнө
parent
commit
6aeda20ca9

+ 3 - 0
Changes

@@ -3,6 +3,9 @@ Revision history for Perl module TestRail::API
 0.026 2015-06-?? TEODESIAN
 0.026 2015-06-?? TEODESIAN
     - Add --no-match option to testrail-tests to find orphan tests in a tree
     - Add --no-match option to testrail-tests to find orphan tests in a tree
     - Upload full raw results to TestRail when not in step_results or case_per_ok mode
     - Upload full raw results to TestRail when not in step_results or case_per_ok mode
+    - Add ability to pass section IDs to discriminate when spawning runs in Test::Rail::Parser
+    - Add ability to pass section names to App::Prove::Plugin::TestRail and testrail-report when spawning runs.
+    - Add sectionNamesToIds convenience method to TestRail::API
 
 
 0.025 2015-05-21 TEODESIAN
 0.025 2015-05-21 TEODESIAN
     - Fix test failures on windows (and an issue in testrail-tests on win32)
     - Fix test failures on windows (and an issue in testrail-tests on win32)

+ 12 - 1
bin/testrail-report

@@ -48,6 +48,9 @@ This should provide sufficient uniqueness to get any run using names.
       If the specified run already exists, the program will simply use the existing run, and disregard the supplied testsuite_id.
       If the specified run already exists, the program will simply use the existing run, and disregard the supplied testsuite_id.
       If the specified plan does not exist, it too will be created for you.
       If the specified plan does not exist, it too will be created for you.
 
 
+    --section [section_name] : When spawning, restrict the cases used in the provided testsuite ID to these sections.
+      Option may be passed multiple times to specify multiple sections.
+
 =head3 CONFIG OPTIONS
 =head3 CONFIG OPTIONS
 
 
 In your $HOME (or the current directory, if your system has no concept of a home directory), put a file called .testrailrc with key=value
 In your $HOME (or the current directory, if your system has no concept of a home directory), put a file called .testrailrc with key=value
@@ -168,6 +171,12 @@ PARAMETERS:
       If the specified plan does not exist, it will be created
       If the specified plan does not exist, it will be created
       as well.
       as well.
 
 
+    --section [section_name] : When spawning, restrict the cases used
+      in the provided testsuite ID to these sections.
+
+      Option may be passed multiple times to specify multiple sections.
+
+
   [CONFIG OVERRIDES]
   [CONFIG OVERRIDES]
   In your \$HOME, (or the current directory, if your system has no
   In your \$HOME, (or the current directory, if your system has no
   concept of a home directory) put a file called .testrailrc with
   concept of a home directory) put a file called .testrailrc with
@@ -218,7 +227,7 @@ TESTING OPTIONS:
 
 
 #Main loop------------
 #Main loop------------
 
 
-my ($help,$apiurl,$user,$password,$project,$run,$case_per_ok,$step_results,$mock,$configs,$plan,$version,$spawn);
+my ($help,$apiurl,$user,$password,$project,$run,$case_per_ok,$step_results,$mock,$configs,$plan,$version,$spawn,$sections);
 
 
 #parse switches
 #parse switches
 GetOptions(
 GetOptions(
@@ -234,6 +243,7 @@ GetOptions(
     'plan=s'         => \$plan,
     'plan=s'         => \$plan,
     'version=s'      => \$version,
     'version=s'      => \$version,
     'spawn=i'        => \$spawn,
     'spawn=i'        => \$spawn,
+    'section=s@'     => \$sections,
     'help'           => \$help
     'help'           => \$help
 );
 );
 
 
@@ -353,6 +363,7 @@ foreach my $phil (@files) {
         'configs'      => $configs,
         'configs'      => $configs,
         'result_options' => $result_options,
         'result_options' => $result_options,
         'spawn'        => $spawn,
         'spawn'        => $spawn,
+        'sections'     => $sections,
         'merge'        => 1
         'merge'        => 1
     });
     });
     $tap->run();
     $tap->run();

+ 13 - 11
lib/App/Prove/Plugin/TestRail.pm

@@ -35,6 +35,7 @@ If \$HOME/.testrailrc exists, it will be parsed for any of these values in a new
     case_per_ok=0
     case_per_ok=0
     step_results=sr_sys_name
     step_results=sr_sys_name
     spawn=123
     spawn=123
+    sections=section1:section2:section3: ... :sectionN
 
 
 Note that passing configurations as filters for runs inside of plans are separated by colons.
 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.
 Values passed in via query string will override values in \$HOME/.testrailrc.
@@ -75,17 +76,18 @@ sub load {
     $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'}  = $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};
-    $ENV{'TESTRAIL_SPAWN'}   = $params->{spawn};
+    $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};
+    $ENV{'TESTRAIL_SPAWN'}    = $params->{spawn};
+    $ENV{'TESTRAIL_SECTIONS'} = $params->{sections};
     return $class;
     return $class;
 }
 }
 
 

+ 28 - 27
lib/Test/LWP/UserAgent/TestRailMock.pm

@@ -229,33 +229,6 @@ $mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4,
 
 
 {
 {
 
 
-$VAR1 = 'index.php?/api/v2/get_suites/9';
-$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:08 GMT',
-                 'client-peer' => '192.168.122.217:80',
-                 'content-length' => '256',
-                 '::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:08 GMT',
-                 'content-type' => 'application/json; charset=utf-8',
-                 'server' => 'Apache/2.4.7 (Ubuntu)'
-               }, 'HTTP::Headers' );
-$VAR5 = '[{"id":9,"name":"HAMBURGER-IZE HUMANITY","description":"Robo-Signed Patriotic People\'s TestSuite","project_id":9,"is_master":false,"is_baseline":false,"is_completed":false,"completed_on":null,"url":"http:\\/\\/testrail.local\\/\\/index.php?\\/suites\\/view\\/9"}]';
-$mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
-
-}
-
-{
-
 $VAR1 = 'index.php?/api/v2/get_suite/9';
 $VAR1 = 'index.php?/api/v2/get_suite/9';
 $VAR2 = '200';
 $VAR2 = '200';
 $VAR3 = 'OK';
 $VAR3 = 'OK';
@@ -498,6 +471,34 @@ $mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4,
 
 
 }
 }
 
 
+{
+
+$VAR1 = 'index.php?/api/v2/get_cases/10&suite_id=9&section_id=9';
+$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' => '322',
+                 '::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":8,"title":"STROGGIFY POPULATION CENTERS","section_id":9,"type_id":6,"priority_id":4,"milestone_id":null,"refs":null,"created_by":1,"created_on":1419364929,"updated_by":1,"updated_on":1419364929,"estimate":null,"estimate_forecast":null,"suite_id":9,"custom_preconds":null,"custom_steps":null,"custom_expected":null}]';
+$mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
+
+}
+
+
 {
 {
 
 
 $VAR1 = 'index.php?/api/v2/get_case/8';
 $VAR1 = 'index.php?/api/v2/get_case/8';

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

@@ -40,6 +40,7 @@ sub make_parser {
     my ($self, $job) = @_;
     my ($self, $job) = @_;
     my $args = $self->SUPER::_get_parser_args($job);
     my $args = $self->SUPER::_get_parser_args($job);
     my @configs = ();
     my @configs = ();
+    my @sections = ();
 
 
     #XXX again, don't see any way of getting this downrange to my parser :(
     #XXX again, don't see any way of getting this downrange to my parser :(
     $args->{'apiurl'}  = $ENV{'TESTRAIL_APIURL'};
     $args->{'apiurl'}  = $ENV{'TESTRAIL_APIURL'};
@@ -49,11 +50,13 @@ sub make_parser {
     $args->{'run'}     = $ENV{'TESTRAIL_RUN'};
     $args->{'run'}     = $ENV{'TESTRAIL_RUN'};
     $args->{'plan'}    = $ENV{'TESTRAIL_PLAN'};
     $args->{'plan'}    = $ENV{'TESTRAIL_PLAN'};
     @configs = split(/:/,$ENV{'TESTRAIL_CONFIGS'}) if $ENV{'TESTRAIL_CONFIGS'};
     @configs = split(/:/,$ENV{'TESTRAIL_CONFIGS'}) if $ENV{'TESTRAIL_CONFIGS'};
-    $args->{'configs'} = \@configs;
+    $args->{'configs'} = \@configs if scalar(@configs);
     $args->{'result_options'} = {'version' => $ENV{'TESTRAIL_VERSION'}} if $ENV{'TESTRAIL_VERSION'};
     $args->{'result_options'} = {'version' => $ENV{'TESTRAIL_VERSION'}} if $ENV{'TESTRAIL_VERSION'};
     $args->{'case_per_ok'}    = $ENV{'TESTRAIL_CASEOK'};
     $args->{'case_per_ok'}    = $ENV{'TESTRAIL_CASEOK'};
     $args->{'step_results'}   = $ENV{'TESTRAIL_STEPS'};
     $args->{'step_results'}   = $ENV{'TESTRAIL_STEPS'};
     $args->{'spawn'}          = $ENV{'TESTRAIL_SPAWN'};
     $args->{'spawn'}          = $ENV{'TESTRAIL_SPAWN'};
+    @sections = split(/:/,$ENV{'TESTRAIL_SECTIONS'}) if $ENV{'TESTRAIL_SECTIONS'};
+    $args->{'sections'} = \@sections if scalar(@sections);
 
 
     #for Testability of plugin
     #for Testability of plugin
     if ($ENV{'TESTRAIL_MOCKED'}) {
     if ($ENV{'TESTRAIL_MOCKED'}) {

+ 39 - 16
lib/Test/Rail/Parser.pm

@@ -74,6 +74,8 @@ Get the TAP Parser ready to talk to TestRail, and register a bunch of callbacks
 
 
 =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.  If the plan does not exist, create it too.
 =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.  If the plan does not exist, create it too.
 
 
+=item B<sections> - ARRAYREF (optional): Restrict a spawned run to cases in these particular sections.
+
 =back
 =back
 
 
 =back
 =back
@@ -112,6 +114,7 @@ sub new {
         'plan'         => delete $opts->{'plan'},
         'plan'         => delete $opts->{'plan'},
         'configs'      => delete $opts->{'configs'} // [],
         'configs'      => delete $opts->{'configs'} // [],
         'spawn'        => delete $opts->{'spawn'},
         'spawn'        => delete $opts->{'spawn'},
+        'sections'     => delete $opts->{'sections'},
         #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'}
@@ -197,21 +200,41 @@ sub new {
 
 
     #If spawn was passed and we don't have a Run ID yet, go ahead and make it
     #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->{'spawn'} && !$tropts->{'run_id'}) {
+        print "# Spawning run\n";
+        my $cases = [];
+        if ($tropts->{'sections'}) {
+            print "# with specified sections\n";
+            #Then translate the sections into an array of case IDs.
+            confess("Sections passed to spawn must be ARRAYREF") unless (reftype($tropts->{'sections'}) || 'undef') eq 'ARRAY';
+            @{$tropts->{'sections'}} = $tr->sectionNamesToIds($tropts->{'project_id'},$tropts->{'spawn'},@{$tropts->{'sections'}});
+            foreach my $section (@{$tropts->{'sections'}}) {
+                my $cases = $tr->getCases($tropts->{'project_id'},$tropts->{'spawn'},$section);
+                push(@$cases,@$cases) if (reftype($cases) || 'undef') eq 'ARRAY';
+            }
+        }
+        if (scalar(@$cases)) {
+            @$cases = map {$_->{'id'}} @$cases;
+        } else {
+            $cases = undef;
+        }
+
         if ($tropts->{'plan'}) {
         if ($tropts->{'plan'}) {
-            $plan = $tr->createRunInPlan( $tropts->{'plan'}->{'id'}, $tropts->{'spawn'}, $tropts->{'run'}, undef, $config_ids );
+            print "# inside of plan\n";
+            $plan = $tr->createRunInPlan( $tropts->{'plan'}->{'id'}, $tropts->{'spawn'}, $tropts->{'run'}, undef, $config_ids, $cases );
             $run = $plan->{'runs'}->[0] if exists($plan->{'runs'}) && (reftype($plan->{'runs'}) || 'undef') eq 'ARRAY' && scalar(@{$plan->{'runs'}});
             $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') {
             if (defined($run) && (reftype($run) || 'undef') eq 'HASH') {
                 $tropts->{'run'} = $run;
                 $tropts->{'run'} = $run;
                 $tropts->{'run_id'} = $run->{'id'};
                 $tropts->{'run_id'} = $run->{'id'};
             }
             }
         } else {
         } else {
-            $run = $tr->createRun( $tropts->{'project_id'}, $tropts->{'spawn'}, $tropts->{'run'}, "Automatically created Run from TestRail::API" );
+            $run = $tr->createRun( $tropts->{'project_id'}, $tropts->{'spawn'}, $tropts->{'run'}, "Automatically created Run from TestRail::API", undef, undef, $cases );
             if (defined($run) && (reftype($run) || 'undef') eq 'HASH') {
             if (defined($run) && (reftype($run) || 'undef') eq 'HASH') {
                 $tropts->{'run'} = $run;
                 $tropts->{'run'} = $run;
                 $tropts->{'run_id'} = $run->{'id'};
                 $tropts->{'run_id'} = $run->{'id'};
             }
             }
         }
         }
         confess("Could not spawn run with requested parameters!") if !$tropts->{'run_id'};
         confess("Could not spawn run with requested parameters!") if !$tropts->{'run_id'};
+        print "# Success!\n"
     }
     }
 
 
     confess("No run ID provided, and no run with specified name exists in provided project/plan!") if !$tropts->{'run_id'};
     confess("No run ID provided, and no run with specified name exists in provided project/plan!") if !$tropts->{'run_id'};
@@ -219,7 +242,7 @@ sub new {
     $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' ) {
         $self->{'file'} = $self->{'_iterator'}->{'command'}->[-1];
         $self->{'file'} = $self->{'_iterator'}->{'command'}->[-1];
-        print "PROCESSING RESULTS FROM TEST FILE: $self->{'file'}\n";
+        print "# PROCESSING RESULTS FROM TEST FILE: $self->{'file'}\n";
         $self->{'track_time'} = 1;
         $self->{'track_time'} = 1;
     } else {
     } else {
         #Not running inside of prove in real-time, don't bother with tracking elapsed times.
         #Not running inside of prove in real-time, don't bother with tracking elapsed times.
@@ -260,13 +283,13 @@ sub unknownCallback {
     if ($line =~ /^Running\s(.*)/) {
     if ($line =~ /^Running\s(.*)/) {
         #TODO figure out which testsuite this implies
         #TODO figure out which testsuite this implies
         $self->{'file'} = $1;
         $self->{'file'} = $1;
-        print "PROCESSING RESULTS FROM TEST FILE: $self->{'file'}\n";
+        print "# PROCESSING RESULTS FROM TEST FILE: $self->{'file'}\n";
     }
     }
     #RAW tap #XXX this regex could be improved
     #RAW tap #XXX this regex could be improved
     if ($line =~ /(.*)\s\.\.\s*$/) {
     if ($line =~ /(.*)\s\.\.\s*$/) {
         $self->{'file'} = $1 unless $line =~ /^[ok|not ok] - /; #a little more careful
         $self->{'file'} = $1 unless $line =~ /^[ok|not ok] - /; #a little more careful
     }
     }
-    print "$line\n" if ($line =~ /^error/i);
+    print "# $line\n" if ($line =~ /^error/i);
 }
 }
 
 
 =head2 commentCallback
 =head2 commentCallback
@@ -313,7 +336,7 @@ sub testCallback {
 
 
     #Don't do anything if we don't want to map TR case => ok or use step-by-step results
     #Don't do anything if we don't want to map TR case => ok or use step-by-step results
     if ( !($self->{'tr_opts'}->{'step_results'} || $self->{'tr_opts'}->{'case_per_ok'}) ) {
     if ( !($self->{'tr_opts'}->{'step_results'} || $self->{'tr_opts'}->{'case_per_ok'}) ) {
-        print "Neither step_results of case_per_ok set.  No action to be taken, except on a whole test basis.\n" if $self->{'tr_opts'}->{'debug'};
+        print "# Neither step_results of case_per_ok set.  No action to be taken, except on a whole test basis.\n" if $self->{'tr_opts'}->{'debug'};
         return 1;
         return 1;
     }
     }
     if ($self->{'tr_opts'}->{'step_results'} && $self->{'tr_opts'}->{'case_per_ok'}) {
     if ($self->{'tr_opts'}->{'step_results'} && $self->{'tr_opts'}->{'case_per_ok'}) {
@@ -336,7 +359,7 @@ sub testCallback {
     my $test_name  = $line;
     my $test_name  = $line;
     my $run_id     = $self->{'tr_opts'}->{'run_id'};
     my $run_id     = $self->{'tr_opts'}->{'run_id'};
 
 
-    print "Assuming test name is '$test_name'...\n" if $self->{'tr_opts'}->{'debug'} && !$self->{'tr_opts'}->{'step_results'};
+    print "# Assuming test name is '$test_name'...\n" if $self->{'tr_opts'}->{'debug'} && !$self->{'tr_opts'}->{'step_results'};
 
 
     my $todo_reason;
     my $todo_reason;
     #Setup args to pass to function
     #Setup args to pass to function
@@ -377,7 +400,7 @@ sub testCallback {
             @{$self->{'tr_opts'}->{'result_custom_options'}->{'step_results'}},
             @{$self->{'tr_opts'}->{'result_custom_options'}->{'step_results'}},
             TestRail::API::buildStepResults($line,"Good result","Bad Result",$status)
             TestRail::API::buildStepResults($line,"Good result","Bad Result",$status)
         );
         );
-        print "Appended step results.\n" if $self->{'tr_opts'}->{'debug'};
+        print "# Appended step results.\n" if $self->{'tr_opts'}->{'debug'};
         return 1;
         return 1;
     }
     }
 
 
@@ -413,7 +436,7 @@ sub EOFCallback {
     }
     }
 
 
     if ($self->{'tr_opts'}->{'case_per_ok'}) {
     if ($self->{'tr_opts'}->{'case_per_ok'}) {
-        print "Nothing left to do.\n";
+        print "# Nothing left to do.\n";
         undef $self->{'tr_opts'} unless $self->{'tr_opts'}->{'debug'};
         undef $self->{'tr_opts'} unless $self->{'tr_opts'}->{'debug'};
         return 1;
         return 1;
     }
     }
@@ -439,7 +462,7 @@ sub EOFCallback {
     my $custom_options = $self->{'tr_opts'}->{'result_custom_options'};
     my $custom_options = $self->{'tr_opts'}->{'result_custom_options'};
 
 
 
 
-    print "Setting results...\n";
+    print "# Setting results...\n";
     my $cres = _set_result($run_id,$test_name,$status,$notes,$options,$custom_options);
     my $cres = _set_result($run_id,$test_name,$status,$notes,$options,$custom_options);
 
 
     undef $self->{'tr_opts'} unless $self->{'tr_opts'}->{'debug'};
     undef $self->{'tr_opts'} unless $self->{'tr_opts'}->{'debug'};
@@ -452,9 +475,9 @@ sub _set_result {
     our $self;
     our $self;
     my $tc;
     my $tc;
 
 
-    print "Test elapsed: ".$options->{'elapsed'}."\n" if $options->{'elapsed'};
+    print "# Test elapsed: ".$options->{'elapsed'}."\n" if $options->{'elapsed'};
 
 
-    print "Attempting to find case by title '".$test_name."'...\n";
+    print "# Attempting to find case by title '".$test_name."'...\n";
     $tc = $self->{'tr_opts'}->{'testrail'}->getTestByName($run_id,$test_name);
     $tc = $self->{'tr_opts'}->{'testrail'}->getTestByName($run_id,$test_name);
     if (!defined($tc) || (reftype($tc) || 'undef') ne 'HASH') {
     if (!defined($tc) || (reftype($tc) || 'undef') ne 'HASH') {
         cluck("ERROR: Could not find test case: $tc");
         cluck("ERROR: Could not find test case: $tc");
@@ -467,14 +490,14 @@ sub _set_result {
 
 
     #Set test result
     #Set test result
     if ($tc) {
     if ($tc) {
-        print "Reporting result of case $xid in run $self->{'tr_opts'}->{'run_id'} as status '$status'...";
+        print "# Reporting result of case $xid in run $self->{'tr_opts'}->{'run_id'} as status '$status'...";
         # createTestResults(test_id,status_id,comment,options,custom_options)
         # createTestResults(test_id,status_id,comment,options,custom_options)
         $cres = $self->{'tr_opts'}->{'testrail'}->createTestResults($tc->{'id'},$status, $notes, $options, $custom_options);
         $cres = $self->{'tr_opts'}->{'testrail'}->createTestResults($tc->{'id'},$status, $notes, $options, $custom_options);
-        print "OK! (set to $status)\n" if (reftype($cres) || 'undef') eq 'HASH';
+        print "# OK! (set to $status)\n" if (reftype($cres) || 'undef') eq 'HASH';
     }
     }
     if (!$tc || ((reftype($cres) || 'undef') ne 'HASH') ) {
     if (!$tc || ((reftype($cres) || 'undef') ne 'HASH') ) {
-        print "Failed!\n";
-        print "No Such test case in TestRail ($xid).\n";
+        print "# Failed!\n";
+        print "# No Such test case in TestRail ($xid).\n";
         $self->{'errors'}++;
         $self->{'errors'}++;
     }
     }
 
 

+ 35 - 1
lib/TestRail/API.pm

@@ -746,6 +746,40 @@ sub getSectionByName {
     return 0;
     return 0;
 }
 }
 
 
+=head2 sectionNamesToIds(project_id,suite_id,names)
+
+Convenience method to translate a list of section names to TestRail section IDs.
+
+=over 4
+
+=item INTEGER C<PROJECT ID> - ID of parent project.
+
+=item INTEGER C<SUITE ID> - ID of parent suite.
+
+=item ARRAY C<NAMES> - Array of section names to translate to IDs.
+
+=back
+
+Returns ARRAY of section IDs.
+
+Throws an exception in the case of one (or more) of the names not corresponding to a valid section name.
+
+=cut
+
+sub sectionNamesToIds {
+    my ($self,$project_id,$suite_id,@names) = @_;
+    confess("Object methods must be called by an instance") unless ref($self);
+    confess("Project ID must be an integer") unless $self->_checkInteger($project_id);
+    confess("Suite ID must be an integer") unless $self->_checkInteger($suite_id);
+    confess("At least one section name must be provided") if !scalar(@names);
+
+    my $sections = $self->getSections($project_id,$suite_id);
+    confess("Invalid project/suite ($project_id,$suite_id) provided.") unless (reftype($sections) || 'undef') eq 'ARRAY';
+    my @ret = grep {defined $_} map {my $section = $_; my @list = grep {$section->{'name'} eq $_} @names; scalar(@list) ? $section->{'id'} : undef} @$sections;
+    confess("One or more user names provided does not exist in TestRail.") unless scalar(@names) == scalar(@ret);
+    return @ret;
+}
+
 =head1 CASE METHODS
 =head1 CASE METHODS
 
 
 =head2 B<getCaseTypes ()>
 =head2 B<getCaseTypes ()>
@@ -1511,7 +1545,7 @@ sub getPlanByID {
     return $self->_doRequest("index.php?/api/v2/get_plan/$plan_id");
     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)>
+=head2 B<createRunInPlan (plan_id,suite_id,name,description,milestone_id,assigned_to_id,config_ids,case_ids)>
 
 
 Create a run.
 Create a run.
 
 

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

@@ -3,7 +3,7 @@
 use strict;
 use strict;
 use warnings;
 use warnings;
 
 
-use Test::More 'tests' => 4;
+use Test::More 'tests' => 5;
 use Test::Fatal;
 use Test::Fatal;
 use App::Prove;
 use App::Prove;
 use App::Prove::Plugin::TestRail;
 use App::Prove::Plugin::TestRail;
@@ -34,3 +34,10 @@ $prove = App::Prove->new();
 $prove->process_args("-PTestRail=apiurl=http://some.testlink.install/,user=someUser,password=somePassword,project=TestProject,plan=bogoPlan,run=bogoRun,version=0.014,case_per_ok=1,spawn=9",'t/skipall.test');
 $prove->process_args("-PTestRail=apiurl=http://some.testlink.install/,user=someUser,password=somePassword,project=TestProject,plan=bogoPlan,run=bogoRun,version=0.014,case_per_ok=1,spawn=9",'t/skipall.test');
 
 
 is (exception {$prove->run()},undef,"Running TR parser spawns both runs and plans");
 is (exception {$prove->run()},undef,"Running TR parser spawns both runs and plans");
+
+$prove = App::Prove->new();
+$prove->process_args("-PTestRail=apiurl=http://some.testlink.install/,user=someUser,password=somePassword,project=TestProject,run=bogoRun,version=0.014,case_per_ok=1,spawn=9,sections=fake.test:CARBON LIQUEFACTION",'t/fake.test');
+
+is (exception {$prove->run()},undef,"Running TR parser can discriminate by sections correctly");
+
+

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

@@ -7,7 +7,7 @@ use Scalar::Util qw{reftype};
 use TestRail::API;
 use TestRail::API;
 use Test::LWP::UserAgent::TestRailMock;
 use Test::LWP::UserAgent::TestRailMock;
 use Test::Rail::Parser;
 use Test::Rail::Parser;
-use Test::More 'tests' => 41;
+use Test::More 'tests' => 48;
 use Test::Fatal qw{exception};
 use Test::Fatal qw{exception};
 
 
 #Same song and dance as in TestRail-API.t
 #Same song and dance as in TestRail-API.t
@@ -375,4 +375,76 @@ $res = exception {
 };
 };
 isnt($res,undef,"TR Parser explodes on instantiation when mutually exclusive options are passed");
 isnt($res,undef,"TR Parser explodes on instantiation when mutually exclusive options are passed");
 
 
+#Check that per-section spawn works
+undef $tap;
+$res = exception {
+    $tap = Test::Rail::Parser->new({
+        'source'              => 't/fake.test',
+        'apiurl'              => $apiurl,
+        'user'                => $login,
+        'pass'                => $pw,
+        'debug'               => $debug,
+        'browser'             => $browser,
+        'run'                 => 'BogoRun',
+        'project'             => 'TestProject',
+        'merge'               => 1,
+        'spawn'               => 9,
+        'sections'            => ['fake.test'],
+        'case_per_ok'         => 1
+    });
+};
+is($res,undef,"TR Parser doesn't explode on instantiation");
+isa_ok($tap,"Test::Rail::Parser");
+
+if (!$res) {
+    $tap->run();
+    is($tap->{'errors'},0,"No errors encountered uploading case results");
+}
+
+#Check that per-section spawn works
+undef $tap;
+$res = exception {
+    $tap = Test::Rail::Parser->new({
+        'source'              => 't/fake.test',
+        'apiurl'              => $apiurl,
+        'user'                => $login,
+        'pass'                => $pw,
+        'debug'               => $debug,
+        'browser'             => $browser,
+        'run'                 => 'BogoRun',
+        'plan'                => 'BogoPlan',
+        'project'             => 'TestProject',
+        'merge'               => 1,
+        'spawn'               => 9,
+        'sections'            => ['fake.test'],
+        'case_per_ok'         => 1
+    });
+};
+is($res,undef,"TR Parser doesn't explode on instantiation");
+isa_ok($tap,"Test::Rail::Parser");
+
+if (!$res) {
+    $tap->run();
+    is($tap->{'errors'},0,"No errors encountered uploading case results");
+}
+
+undef $tap;
+$res = exception {
+    $tap = Test::Rail::Parser->new({
+        'source'              => 't/fake.test',
+        'apiurl'              => $apiurl,
+        'user'                => $login,
+        'pass'                => $pw,
+        'debug'               => $debug,
+        'browser'             => $browser,
+        'run'                 => 'BogoRun',
+        'project'             => 'TestProject',
+        'merge'               => 1,
+        'spawn'               => 9,
+        'sections'            => ['potzrebie'],
+        'case_per_ok'         => 1
+    });
+};
+isnt($res,undef,"TR Parser explodes on instantiation with invalid section");
+
 0;
 0;

+ 5 - 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 => 71;
+use Test::More tests => 73;
 use Test::Fatal;
 use Test::Fatal;
 use Test::Deep;
 use Test::Deep;
 use Scalar::Util ();
 use Scalar::Util ();
@@ -56,7 +56,6 @@ my @cuser_ids = $tr->userNamesToIds(@user_names);
 cmp_deeply(\@cuser_ids,\@user_ids,"userNamesToIds functions correctly");
 cmp_deeply(\@cuser_ids,\@user_ids,"userNamesToIds functions correctly");
 isnt(exception {$tr->userNamesToIds(@user_names,'potzrebie'); }, undef, "Passing invalid user name throws exception");
 isnt(exception {$tr->userNamesToIds(@user_names,'potzrebie'); }, undef, "Passing invalid user name throws exception");
 
 
-
 #Test PROJECT methods
 #Test PROJECT methods
 my $project_name = 'CRUSH ALL HUMANS';
 my $project_name = 'CRUSH ALL HUMANS';
 
 
@@ -86,6 +85,10 @@ ok($tr->getSections($new_project->{'id'},$new_suite->{'id'}),"Can get section li
 is($tr->getSectionByName($new_project->{'id'},$new_suite->{'id'},$section_name)->{'name'},$section_name,"Can get section by name");
 is($tr->getSectionByName($new_project->{'id'},$new_suite->{'id'},$section_name)->{'name'},$section_name,"Can get section by name");
 is($tr->getSectionByID($new_section->{'id'})->{'id'},$new_section->{'id'},"Can get new section by id");
 is($tr->getSectionByID($new_section->{'id'})->{'id'},$new_section->{'id'},"Can get new section by id");
 
 
+my @cids = $tr->sectionNamesToIds($new_project->{'id'},$new_suite->{'id'},$section_name);
+is($cids[0],$new_section->{'id'},"sectionNamesToIds returns correct IDs");
+isnt(exception {$tr->sectionNamesToIds($new_project->{'id'},$new_suite->{'id'},"No such Section");},undef,"Passing bogus section to sectionNamesToIds throws exception");
+
 #Test CASE methods
 #Test CASE methods
 my $case_name = 'STROGGIFY POPULATION CENTERS';
 my $case_name = 'STROGGIFY POPULATION CENTERS';
 my $new_case = $tr->createCase($new_section->{'id'},$case_name);
 my $new_case = $tr->createCase($new_section->{'id'},$case_name);

+ 4 - 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' => 135;
+use Test::More 'tests' => 138;
 use Test::Fatal;
 use Test::Fatal;
 use Class::Inspector;
 use Class::Inspector;
 use Test::LWP::UserAgent;
 use Test::LWP::UserAgent;
@@ -48,6 +48,7 @@ isnt( exception {$tr->getRunByID() },undef,'getRunByID returns error when no arg
 isnt( exception {$tr->getRunByName() },undef,'getRunByName returns error when no arguments are passed');
 isnt( exception {$tr->getRunByName() },undef,'getRunByName returns error when no arguments are passed');
 isnt( exception {$tr->getSectionByID() },undef,'getSectionByID returns error when no arguments are passed');
 isnt( exception {$tr->getSectionByID() },undef,'getSectionByID returns error when no arguments are passed');
 isnt( exception {$tr->getSectionByName() },undef,'getSectionByName returns error when no arguments are passed');
 isnt( exception {$tr->getSectionByName() },undef,'getSectionByName returns error when no arguments are passed');
+isnt( exception {$tr->sectionNamesToIds() },undef,'sectionNamesToIds returns error when no arguments are passed');
 isnt( exception {$tr->getTestByID() },undef,'getTestByID returns error when no arguments are passed');
 isnt( exception {$tr->getTestByID() },undef,'getTestByID returns error when no arguments are passed');
 isnt( exception {$tr->getTestByName() },undef,'getTestByName returns error when no arguments are passed');
 isnt( exception {$tr->getTestByName() },undef,'getTestByName returns error when no arguments are passed');
 isnt( exception {$tr->getTestResults() },undef,'getTestResults returns error when no arguments are passed');
 isnt( exception {$tr->getTestResults() },undef,'getTestResults returns error when no arguments are passed');
@@ -121,6 +122,7 @@ isnt(exception {$tr->getPlanByName(1)}, undef,'getPlanByName with 1 arg returns
 isnt(exception {$tr->getRunByName(1)}, undef,'getRunByName with 1 arg returns error');
 isnt(exception {$tr->getRunByName(1)}, undef,'getRunByName with 1 arg returns error');
 isnt(exception {$tr->getSectionByName(1)}, undef,'getSectionByName with 1 arg returns error');
 isnt(exception {$tr->getSectionByName(1)}, undef,'getSectionByName with 1 arg returns error');
 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->sectionNamesToIds(1) },undef,'sectionNamesToIds returns error when 1 arguments are passed');
 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');
 isnt(exception {$tr->getChildRunByName({}) },undef,'getChildRunByName returns error when 1 argument passed');
@@ -147,6 +149,7 @@ isnt(exception {$tr->createSection(1,1)}, undef,'createSection with 2 args retur
 isnt(exception {$tr->getCaseByName(1,1)}, undef,'getCaseByName 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->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->getSectionByName(1,1)}, undef,'getSectionByName with 2 args returns error');
+isnt(exception {$tr->sectionNamesToIds(1,1) },undef,'sectionNamesToIds returns error when 2 arguments are passed');
 isnt( exception {$tr->createRunInPlan(1,1) },undef,'createRunInPlan returns error when 2 arguments passed');
 isnt( exception {$tr->createRunInPlan(1,1) },undef,'createRunInPlan returns error when 2 arguments passed');
 
 
 #3 arg functions
 #3 arg functions

+ 8 - 1
t/testrail-report.t

@@ -1,7 +1,7 @@
 use strict;
 use strict;
 use warnings;
 use warnings;
 
 
-use Test::More 'tests' => 12;
+use Test::More 'tests' => 14;
 
 
 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 @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`;
 my $out = `@args`;
@@ -36,6 +36,13 @@ is($? >> 8, 0, "Exit code OK reported with spawn");
 $matches = () = $out =~ m/Reporting result of case.*OK/ig;
 $matches = () = $out =~ m/Reporting result of case.*OK/ig;
 is($matches,2,"Attempts to spawn work");
 is($matches,2,"Attempts to spawn work");
 
 
+#Test that spawn works w/sections
+@args = ($^X,qw{bin/testrail-report --apiurl http://testrail.local --user "test@fake.fake" --password "fake" --project "TestProject" --run "TestingSuite2" --spawn 9 --case-ok --section "CARBON LIQUEFACTION" --mock t/test_subtest.tap});
+$out = `@args`;
+is($? >> 8, 0, "Exit code OK reported with spawn");
+$matches = () = $out =~ m/with specified sections/ig;
+is($matches,1,"Attempts to spawn work");
+
 #Test that help works
 #Test that help works
 @args = ($^X,qw{bin/testrail-report --help});
 @args = ($^X,qw{bin/testrail-report --help});
 $out = `@args`;
 $out = `@args`;