Subversion Repositories DevTools

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
4762 rpuchmay 1
########################################################################
2
# Copyright (C) 2010 ERG Limited, All rights reserved
3
#
4
# Module name   : jats_utf_ant.pl
5
# Module type   : Makefile system
6
# Compiler(s)   : Perl
7
# Environment(s): jats
8
#
9
# Description   : Jats utility to read Ant test result output files and
10
#                 reformats them into standard XML for JATS to import
11
#                 into the Release Manager database.
12
#
13
# Usage         : perl jats_utf_ant.pl [-h] [-v] -o <folder> -t <target>
14
#
15
#......................................................................#
16
 
17
require 5.006_001;   # to remain compatible with installed per version
18
use diagnostics;
19
use strict;
20
use warnings;
21
 
22
use Pod::Usage;
23
use Getopt::Long;
24
use Time::HiRes;
25
use File::Spec;
26
use XML::Simple;
27
use Cwd;
28
 
29
use JatsError;
30
use JatsVersionUtils;
31
use JatsRmApi;
32
use JatsSystem;
33
 
34
my $VERSION = "1.0.0"; # Update this every time this file changes
35
 
36
#   Run main() with the command line arguments unless
37
#   we're being used by a 'require'
38
main(@ARGV) unless caller;
39
 
40
#------------------------------------------------------------------------------
41
# Function    : main
42
#
43
# Description : The main-line as a function
44
#               Making main a subroutine allows me to test all
45
#               the others without running main
46
#
47
#               Because this is MAIN the indentation rule is 
48
#               violated and all lines are unindented by one
49
#               indentation level
50
#
51
#               Processes any result files from Junit as run by ANT.
52
#
53
#               These result files are named 'Test-<name>.xml'.
54
#
55
#               Parse these files and extract the pass/fail information as
56
#               well as details for failed tests. This information is written,
57
#               as XML, to the output folder specified with the -o switch. The
58
#               files are named <Target>-<Time>.xml, where <Target> is
59
#               specified using the -t switch, and <Time> is the current unix
60
#               time to 5 decimal places with the decimal point removed. e.g.
61
#               Win32-141860573651218.xml
62
#
63
#               The output XML file looks like:
64
#
65
# <TestResults>
66
#   <!-- a passed test is a self closing TestResult -->
67
#   <TestResult DURATION="0.01" NAME="java.path::method" OUTCOME="PASS" />
68
#   <!-- a failed test has a closing tag and some content -->
69
#   <TestResult DURATION="0.01" NAME="java.path::method" OUTCOME="FAILURE">
70
#     some
71
#     text
72
#     denoting
73
#     a stack dump
74
#     or other
75
#     details
76
#   </TestResult>
77
#   <!-- an errored test has a closing tag and some content -->
78
#   <TestResult DURATION="0.01" NAME="java.path::method" OUTCOME="ERROR">
79
#     some
80
#     text
81
#     denoting
82
#     a stack dump
83
#     or other
84
#     details
85
#   </TestResult>
86
# </TestResults>
87
#
88
# Inputs      : ARGV (see above)
89
#
90
# Output      : The standard XML format file for JATS to read
91
#
92
# Returns     : 0 - there were some failed tests
93
#               1 - all tests passed
94
#
95
sub main {
96
 
97
#   Start of broken indentation convention
98
 
99
our ($opt_help, $opt_verbose, $opt_outFolder, $opt_target);
100
$opt_help = 0;
101
$opt_verbose = 0;
102
 
103
my $result = GetOptions (
104
    "help|h:+"          => \$opt_help,
105
    "verbose|v:+"       => \$opt_verbose,
106
    "out|o=s"           => \$opt_outFolder,
107
    "target|t=s"        => \$opt_target,
108
);
109
 
110
#
111
#   Process help and manual options
112
#
113
pod2usage(-verbose => 0, -message => "Version: $VERSION")  if ($opt_help == 1);
114
pod2usage(-verbose => 1)  if ($opt_help == 2);
115
pod2usage(-verbose => 2)  if ($opt_help > 2);
116
 
117
#
118
#   Configure the error reporting process now that we have the user options
119
#
120
ErrorConfig( 'name'    =>'UTF_ANT',
121
             'verbose' => $opt_verbose );
122
 
123
#   Set defaults and error checking
124
$opt_outFolder = '.' unless ($opt_outFolder);
125
Error ("Must specify a target platform (e.g. -t 'WIN32')") unless($opt_target);
126
Error ("Output Folder \'$opt_outFolder\' does not exist") unless (-d $opt_outFolder);
127
 
128
return doMain($opt_outFolder, $opt_target);
129
 
130
#   End of broken indentation convention
131
}
132
 
133
#------------------------------------------------------------------------------
134
# Function    : doMain
135
#
136
# Description : perform the actions required by this module as described above
137
#
138
# Inputs      : $outFolder - the folder wehre to place the output XML file.
139
#               $target - the target platform name.
140
#                         This is used in generating the output file name as
141
#                         described above.
142
#
143
# Output      : The result XML file.
144
#
145
# Returns     : 0 - some tests failed
146
#               1 - all tests passed
147
#
148
sub doMain {
149
    my ($outFolder, $target) = @_;
150
    my ($passed, @instance) = createBuildInstance($target);
151
    outputJatsXmlFile($outFolder, $target, @instance);
152
    return $passed;
153
}
154
 
155
#------------------------------------------------------------------------------
156
# Function    : outputJatsXmlFile
157
#
158
# Description : Write the result structure to the output XML file.
159
#
160
# Inputs      : $output_folder - where to put the output file.
161
#               $target - the target platforn, used to construct the output
162
#                         filename.
163
#               @instance - the results read from the Ant file.
164
#
165
# Output      : The result XML file.
166
#
167
# Returns     : The filename of the output file.
168
#
169
sub outputJatsXmlFile {
170
    my ($output_folder, $target, @instance) = @_;
171
    my %xml;
172
 
173
    #   Each element in @instance is put into a <TestResult> XML element
174
    @{$xml{TestResult}} = @instance;
175
    #   The 'MESSAGE' key for failed tests forms the content of the
176
    #   <TestResult> element. Other keys are converted to attributes.
177
    #   Assign <TestResults> as the root XMl node.
178
    my $xmlData = XMLout(\%xml, ContentKey => 'MESSAGE', RootName => 'TestResults');
179
 
180
    #   Construct the output filename from the microsecond time.
181
    my $time = Time::HiRes::time;
182
    $time =~ s/\.//;
183
    #   Append enough '0' to make 15 chars. This make uniform length numbers
184
    #   and allows filename sorting.
185
    $time .= "0"x(15-length($time));
186
    my $filename = "$output_folder/$target-$time.xml";
187
    Error("$filename already exists: $!\n") if -e $filename;
188
 
189
    #   Write the data to the XML file.
190
    open ( my $outFile, ">", $filename)
191
        || Error("Cannot open $filename for writing: $!\n");
192
    print $outFile $xmlData;
193
    return $filename;
194
}
195
 
196
#------------------------------------------------------------------------------
197
# Function    : createBuildInstance
198
#
199
# Description : Reads an Ant test results file and returns an array with an
200
#               entry per test. Each entry is a Hash as described below.
201
#
202
# Inputs      : $target - the build target platform. Cannot be undef
203
#
204
# Output      : None
205
#
206
# Returns     : A pair of values ($passed, $test_results)
207
#               $passed - 1 if there were no failed tests
208
#                         0 if there were failed tests
209
#               @test_results - A data structure consisting of an array of
210
#                               hashes
211
#                               Each entry in the hash holds:
212
#                   NAME - the class and method name of the test (or as
213
#                          much information as is necessary to uniquely
214
#                          identify the test)
215
#                   OUTCOME - one of 'UNKNOWN', 'PASS', 'FAIL', 'ERROR'.
216
#                             Being the test result.
217
#                   DURATION - the time (in seconds) that the test took to run
218
#                   TARGET_PLATFORM - The platform that was used to run the test
219
#                   MESSAGE - if the test did not pass, there is more
220
#                             information here. Most likely a stack dump.
221
#
222
sub createBuildInstance {
223
    my ($target) = @_;
224
    Error("Must provide a target") unless defined($target);
225
 
226
    my $filename = findAntResultsFile();
227
    my ($passed, @test_results) = parseTestRun($filename, $target);
228
 
229
    return ($passed, @test_results);
230
}
231
 
232
#------------------------------------------------------------------------------
233
# Function    : findAntResultsFile
234
#
235
# Description : Find a file matching the pattern '^Test-.*\.xml' below the
236
#               current folder.
237
#
238
# Inputs      : none
239
#
240
# Output      : none
241
#
242
# Returns     : The complete path and filename of the first matching file.
243
#
244
sub findAntResultsFile {
245
    my $testResultsFile;
246
    use File::Find;
247
 
248
    find(sub {
249
            if (/^Test-.*\.xml/) {
250
                #   Get absolute path
251
                $testResultsFile = File::Spec->rel2abs("$_");
252
                #   Exit once we've found one file (speed optimisation)
253
                goto JATS_UTF_ANT_FOUND;
254
            }
255
        }, '.');
256
JATS_UTF_ANT_FOUND:  # goto here from inside Find once the file is found
257
 
258
    return $testResultsFile;
259
}
260
 
261
#------------------------------------------------------------------------------
262
# Function    : parseTestRun
263
#
264
# Description :
265
#
266
# Inputs      : $filename - the junit results.xml file to parse
267
#               $target - the build target platform to assign to each test
268
#
269
# Output      : none
270
#
271
# Returns     : a pair of values ($passed, @test_results)
272
#               $passed - 1 if all tests passed
273
#                         0 if some tests failed
274
#               @test_results - A list of TestResult's (see above)
275
#          
276
sub parseTestRun {
277
    my ($filename, $target) = @_;
278
    my ($passed, @test_results);
279
    my ($project_name, $package_name, $package_version, $timestamp);
280
    $passed = 1;
281
 
282
    open( my $infile, "<$filename" ) || Error ( "Cannot read from $filename", $! );
283
    #   Read the file, line by line
284
    while ( <$infile> ) {
285
 
286
        #   Extract one test case
287
        #
288
        #   This may progress the file pointer if <testcase>...</testcase>
289
        #   is multiline
290
        my @test_case = getTestCase($_, $infile) if /\<testcase/;
291
 
292
        #   Parse the test case creating a hash
293
        my %test_run = parseTestCase($target, @test_case) if (@test_case);
294
 
295
        #   Save the test result in the array
296
        push(@test_results, {%test_run}) if (%test_run);
297
 
298
        #   Record that there was at least one failed test
299
        $passed = 0 if (%test_run && ($test_run{OUTCOME} ne 'PASS'));
300
    }
301
    return ($passed, @test_results);
302
}
303
 
304
#------------------------------------------------------------------------------
305
# Function    : getTestCase
306
#
307
# Description : Reads from the file, and advances the file pointer, until the
308
#               closing '</testcase>' tag is read.
309
#
310
# Inputs      : $line - the current line in the results.xml file. This line
311
#                       will contain '<testcase'.
312
#               $file - the file handle of the results.xml file.
313
#
314
# Output      : none
315
#
316
# Returns     : A string array of all lines read, including the start and end
317
#               'testcase' tag.
318
#
319
sub getTestCase {
320
    my ($line, $file) = @_;
321
    my (@result);
322
 
323
    #   Save the first line, containing the opening <testcase> tag
324
    push(@result, $line);
325
 
326
    #   No more to do if it's all on one line
327
    return @result if ($line =~/\<\/testcase\>/);
328
 
329
    #   Save subsequent lines up to and including the closing </testcase> tag
330
    while (<$file>)
331
    {
332
        push (@result, $_);
333
        last if /\<\/testcase\>/;
334
    }
335
 
336
    return @result;
337
}
338
 
339
#------------------------------------------------------------------------------
340
# Function    : getDetails
341
#
342
# Description :
343
#
344
# Inputs      : $line - a line of XML containing all the attributes of the
345
#                       <testcase> tag.
346
#
347
# Output      : none
348
#
349
# Returns     : A tuple of values ($name, $duration, $outcome)
350
#               $name - The test name, concatenated from the 'classname' and
351
#                       'name' attributes.
352
#               $duration - The test duration, in seconds, from the 'time'
353
#                           attribute.
354
#               $outcome - The test outcome (= 'PASS') if we know it (i.e. the
355
#                          closing </testcase> tag is on the same line).
356
#                          Otherwise, if we don't know it, return undef.
357
#
358
sub getDetails {
359
    my ($line) = shift; 
360
 
361
    #   Pattern to extract a thing between two quotes (' or ").
362
    my ($xml_value) = qr/["\']([^"\']*)["\']/;
363
 
364
    my ($name, $duration, $outcome);
365
 
366
    if ($line =~ /\sclassname=${xml_value}\s*name=${xml_value}\s*time=${xml_value}/) {
367
        $name = $1.'::'.$2;
368
        $duration = $3;
369
        $outcome = 'PASS' if $line =~ /\<\/testcase\>/;
370
    }
371
 
372
    return ($name, $duration, $outcome);
373
}
374
 
375
#------------------------------------------------------------------------------
376
# Function    : parseMessage
377
#
378
# Description :
379
#
380
# Inputs      : $pattern - The XML element name from which to extract the
381
#                          message.
382
#               $line - The line with the open tag. E.g.
383
#                          <error ...>
384
#               @lines - all lines until, and including, the closing tag. E.g.
385
#                            ...
386
#                            ...
387
#                          </error>
388
#
389
# Output      : none
390
#
391
# Returns     : The value of the matched element.
392
#
393
sub parseMessage {
394
    my ($pattern, $line, @lines) = @_;
395
    my ($message);
396
 
397
    if ($line =~ /\<${pattern} /) {
398
        my $temp_message = $line;
399
 
400
        #    consume until </$pattern>
401
        while ($line = shift @lines) {
402
            $temp_message .= $line;
403
            last if $line =~ /\<\/${pattern}\>/;
404
        }
405
 
406
        #   Extract between '<pattern ...>' and '</pattern>'
407
        $temp_message =~ m/\<${pattern}[^>]*>([^\<]*)\<\/${pattern}>/;
408
 
409
        $message = $1;
410
    }
411
    return ($message);
412
}
413
 
414
#------------------------------------------------------------------------------
415
# Function    : parseTestCase
416
#
417
# Description : Takes a <testCase> element and parses it into a hash.
418
#
419
# Inputs      : A tuple of ($test_target, @lines)
420
#               $test_target - The target platform, e.g. 'Win32'.
421
#               @lines - The lines from the file from the opening, to the
422
#                        closing <testCase> tag (inclusive).
423
#
424
# Output      : none
425
#
426
# Returns     : A hash with the following keys:
427
#                 TARGET_PLATFORM - What was passed in as $testTarget.
428
#                 NAME - the test method name.
429
#                 DURATION - the test duration, in seconds.
430
#                 OUTCOME - one of 'PASS', 'FAILED', 'ERROR'
431
#
432
sub parseTestCase {
433
    my $testTarget = shift;
434
    my %testRun;
435
    $testRun{TARGET_PLATFORM} = $testTarget;
436
    while (my $line = shift @_) {
437
        my ($name, $duration, $outcome, $message);
438
        ($name, $duration, $outcome) = getDetails($line);
439
        if (defined($name) && defined($duration)) {
440
            $testRun{NAME} = $name;
441
            $testRun{DURATION} = $duration;
442
            $testRun{OUTCOME} = $outcome if (defined($outcome));
443
            next;
444
        }
445
        last if $line =~ /\<\/testcase\>/;
446
        ($message) = parseMessage(qr/error/  , $line, @_);
447
        if (defined($message)) {
448
            $testRun{OUTCOME} = 'ERROR';
449
            $testRun{MESSAGE} = $message;
450
            next;
451
        }
452
        ($message) = parseMessage(qr/failure/, $line, @_);
453
        if (defined($message)) {
454
            $testRun{OUTCOME} = 'FAILURE';
455
            $testRun{MESSAGE} = $message;
456
            next;
457
        }
458
    }
459
    return %testRun;
460
}
461
 
462
1;
463
 
464
=pod 1
465
 
466
=head1 NAME
467
 
468
utf_ant - Parse Ant Junit test results for import to Release manager.
469
 
470
=head1 SYNOPSIS
471
 
472
  jats utf_ant [options]
473
 
474
 Options:
475
    -help        - Help message (may be repeated)
476
    -verbose     - Level of user messages output during processing.
477
    -out=path    - The output folder where to generate the XML file.
478
    -target=name - The target platform for which the tests were run.
479
 
480
=head1 OPTIONS
481
 
482
=over 8
483
 
484
=item B<-help>
485
 
486
Print a help message and exists. This switch may be repeated up to three
487
times to provide more detailed help.
488
 
489
E.g.
490
jats utf_ant -help
491
jats utf_ant -help -help -help
492
 
493
=item B<-verbose>
494
 
495
This option will display progress information as the program executes.
496
 
497
=item B<-out=path>
498
 
499
This is a mandatory option, that tells the program where to put the output
500
file.
501
 
502
=item B<-target=name>
503
 
504
This is a mandatory option, that tells the program the target platform for
505
which the tests were run. The output filename has this as its prefix.
506
 
507
=back
508
 
509
=head1 DESCRIPTION
510
 
511
This program is used to allow the Release manager to store test results
512
obtained from JUnit run by Ant.
513
 
514
It converts ANT JUnit output to a standard XML format for importing in
515
to the Release Manager database using another program.
516
 
517
The intent is that there is a suite of these converters that all output
518
the same format XML, that can then be imported to the Release Manager
519
database.
520
 
521
e.g. mstest_utf - which converts MsTest output to the desired XML format.
522
 
523
The Ant result file is looked for in the current folder and any
524
sub-folders. The file is identified by a pattern which is:
525
        'Test-.*\.xml'
526
 
527
The first matching file is used, and no more are processed.
528
 
529
The output is stored in the given output folder, which may be a relative
530
or absolute path. The output filename is derived from the given target
531
platform and a high resolution timestamp. E.g.
532
        './result/Win32-141860573651218.xml'
533
 
534
=head2 Examples
535
 
536
Process the Ant file and place the output in the 'result' folder for the 'Ubunut12' target.
537
 
538
    jats utf_and -out=result -target='Ubuntu12'
539
 
540
This will generate the file:
541
    ./result/Ubuntu12-<time>.xml
542
 
543
=cut