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