File Coverage

blib/lib/Integrator/Module/Build.pm
Criterion Covered Total %
statement 28 30 93.3
branch n/a
condition n/a
subroutine 10 10 100.0
pod n/a
total 38 40 95.0


line stmt bran cond sub pod time code
1             package Integrator::Module::Build;
2 9     9   248672 use warnings;
  9         24  
  9         370  
3 9     9   53 use strict;
  9         16  
  9         414  
4              
5             =head1 NAME
6              
7             Integrator::Module::Build - Gather and synchronize Test::More results in Cydone's Integrator
8              
9             =head1 VERSION
10              
11             Version $Revision: 1.57 $
12              
13             =cut
14              
15 9     9   50 use vars qw($VERSION);
  9         21  
  9         822  
16             $VERSION = sprintf "%d.%03d", q$Revision: 1.57 $ =~ /(\d+)/g;
17              
18 9     9   9778 use File::stat;
  9         117179  
  9         79  
19 9     9   9259 use Data::UUID;
  9         37539  
  9         848  
20 9     9   16154 use Data::Dumper;
  9         80855  
  9         736  
21 9     9   11524 use MIME::Base64;
  9         8294  
  9         794  
22 9     9   10686 use Term::ReadKey;
  9         50557  
  9         998  
23 9     9   18854 use LWP::UserAgent;
  9         564729  
  9         376  
24 9     9   14167 use XML::Simple qw{:strict};
  0            
  0            
25              
26             use Test::TAP::Model;
27             use Integrator::Test::TAP::Model::Patch;
28              
29             use base 'Module::Build';
30              
31             our $NEW_LINE = '__NL__'; # special encoding for '\n'
32             our $POUND_SIGN = '__PS__'; # special encoding for '#'
33              
34             $|=1;
35              
36             =head1 SYNOPSIS
37              
38             This module is used to construct perl test harnesses suitable for
39             use with Cydone's Integrator framework. A test harness created with
40             Integrator::Module::Build can communicate test results in the style
41             of Test::More to Cydone Integrator and synchronise test information
42             (test cases, descriptions, results, log files, measurements, component
43             states, etc.)
44              
45             Since the test harness itself is nothing less than a standard perl
46             module, you can use Module::Start to create a new test harness. Here
47             is an example on how to create a test harness called 'my-test-module'
48             using the module-starter script available from Module::Starter:
49              
50             module-starter --mb \
51             --email='my-email@cydone.com' \
52             --author='User Name' \
53             --module='My::Test::Module'
54              
55             Then, you want to edit the Build.PL file under My-Test-Module with the
56             proper Integrator credentials (go to L to
57             request your demo credentials, a specific Build.PL file will be sent
58             to you within a demo test harness).
59              
60             Here is a typical Build.PL file used to instantiate such a perl test
61             harness (note the 'Integrator::Module::Build' lines):
62              
63             use strict;
64             use warnings;
65             use Integrator::Module::Build;
66              
67             my $builder = Integrator::Module::Build->new(
68             module_name => 'My::Test::Module',
69             dist_author => ' ',
70            
71             integrator_project_code => 'demo',
72             integrator_lab_code => 'default',
73             integrator_user => 'your username',
74             integrator_pwd => 'the password you received',
75             integrator_url => 'https://public1.partnerscydone.com',
76             integrator_http_realm => '',
77             integrator_http_user => '',
78             integrator_http_pwd => '',
79             );
80              
81             $builder->create_build_script();
82              
83             You can now create/edit the test case files (based on Test::Simple or
84             Test::More style) under the ./t directory in your new test module and
85             synchronize the results in Integrator.
86              
87             To execute the test cases and synchronize the results do:
88              
89             perl ./Build.PL
90             ./Build
91             ./Build integrator_test
92             ./Build integrator_sync
93              
94             =cut
95              
96             =head1 EXPORT
97              
98             Integrator::Module::Build exports a set of actions available from the
99             command line 'Build' script, in the style of what Module::Build does.
100              
101             Integrator::Module Build supports all actions from Module::Build and
102             adds specific actions to execute tests in the local database as well as
103             to synchronise test results with Cydone's Integrator centralized server.
104              
105             The specific actions of Integrator::Module::Build are:
106              
107             Usage: ./Build arg1=value arg2=value ...
108             Example: ./Build test verbose=1
109              
110             Actions defined:
111             integrator_test
112             integrator_sync
113             integrator_version
114              
115             integrator_send_xml
116             integrator_store
117             integrator_xml_report
118             integrator_download_test_definition
119             integrator_upload_test_definition
120              
121             As of July 2007, Module::Build was in revision 0.2808. The most common
122             actions inherited from Module::Build are:
123              
124             dist - used to wrap a complete test module into a tar.gz. file.
125             (this is very useful to distribute your tests in a portable module)
126             help - displays a help message
127              
128             Actions starting with 'integrator' are defined in this document. Please
129             refer to Module::Build by Ken Williams available at L
130             for the other actions, as they are more related to module distribution,
131             installation and maintenance.
132              
133             =cut
134              
135             # This begin block is used to substitute a portable (but slower) version
136             # of the MD5 module if the original could not be found.
137             BEGIN {
138             eval {
139             require Digest::MD5;
140             import Digest::MD5 'md5_hex'
141             };
142             if ($@) { # oups, no Digest::MD5
143             require Digest::Perl::MD5;
144             import Digest::Perl::MD5 'md5_hex'
145             }
146             }
147              
148             ### # We need to protect the credentials of the harness user since they
149             ### # could be stored locally. But the warning is not required when first
150             ### # installing IMB. Hence we test Build.PL to see if it contains IMB before
151             ### # going any further. Then we can issue the warning if the credentails
152             ### # are not properly protected.
153             ### if (_file_contains_text('Build.PL', qr{Integrator::Module::Build\s*->\s*new})) {
154             ### foreach my $file (qw{ Build.PL _build/build_params}) {
155             ### warn( "SECURITY WARNING: '$file' is readable or writeable by others, ".
156             ### "change file permissions with 'chmod 700 $file'")
157             ### unless (_is_rw_safe($file));
158             ### }
159             ### }
160              
161             =head1 ACTIONS
162              
163             Since this module is used to generate a local 'Build' file called with
164             actions, we first document these actions. Please note that all the actions
165             are called from the command line without the 'ACTION_' prefix as in:
166              
167             ./Build integrator_test --test_files=./t/00-load.t
168             #launches one test and logs the result locally
169             #notice the bare 'integrator_test' action
170              
171             =head2 ACTION_integrator_test
172              
173             used with: ./Build integrator_test
174             ./Build integrator_test --test_files=./t/00-load.t \
175             --test_files=./t/tc001_v1_security.t \
176             --test_files=./t/tc002_v3_robustness.t
177              
178             This action is used to start a test run and gather the results locally
179             for later upload and analysis in Cydone Integrator. Each sucessive
180             invocation is logged as a unique test run and can latter be uploaded with
181             the 'integrator_sync' action.
182              
183             =cut
184            
185             sub ACTION_integrator_test {
186             my ($self) = @_;
187             my $p = $self->{properties};
188            
189             $self->depends_on('code');
190            
191             # Make sure we test the module in blib/
192             local @INC = (File::Spec->catdir($p->{base_dir}, $self->blib, 'lib'),
193             File::Spec->catdir($p->{base_dir}, $self->blib, 'arch'),
194             @INC);
195              
196             ### save all the parameters
197             $p->{integrator_test_signatures} = _compute_test_signatures($self);
198             $p->{integrator_pwd} = '***********'; #we hide the password in the test data...
199             $p->{integrator_module_build_version} = $Integrator::Module::Build::VERSION;
200             $p->{integrator_os_env} = \%ENV;
201             delete $p->{build_requires}; #unfortunate, but we don't pass it through since hash keys contains
202             # '::' (as in 'Test::More') and it breaks the xml
203              
204             $self->config_data('integrator_harness_info' => $p );
205              
206             # Filter out nonsensical @INC entries - some versions of
207             # Test::Harness will really explode the number of entries here
208             @INC = grep {ref() || -d} @INC if @INC > 100;
209              
210             my $tests = $self->find_test_files;
211             my $t = Integrator::Test::TAP::Model::Patch->new_with_struct();
212             $t->log_time( 1 eq 1 );
213              
214             if (@$tests) {
215             $t->run_tests(@$tests);
216             my $time = time;
217             _log_results_with_look_up($self, $t);
218             my $delay = time - $time;
219             print "Data saved localy in $delay sec.\n";
220             } else {
221             print("No tests defined.\n");
222             }
223             }
224              
225             =head2 ACTION_integrator_sync
226              
227             used with: ./Build integrator_sync
228              
229             This action will send locally generated test run data to the
230             web server and will clean-up local data when the transaction is
231             completed. Aditionally, all configuration data related to the local
232             test files will be updated (note: the configuration data sync is not
233             yet implemented as of March 2006).
234              
235             Credentials are looked-up from the local Build.PL configuration file or
236             prompted from the user if required.
237              
238             All local configuration (ENV variables, build parameters, server
239             configuration) are also uploaded to the server to ensure test
240             traceability. Please refer to Cydone Integrator for a detailed list of
241             the information that is sent to the server.
242              
243             =cut
244              
245             # This sub also sends the MD5 and the local configuration to the server.
246              
247             sub ACTION_integrator_sync {
248             my $self = shift;
249             my $xml = shift;
250             my $t = time;
251             my $store_mode='YES'; #initial assumption
252            
253             my $credentials = _get_integrator_credentials($self);
254              
255             print "== LOCAL DATABASE LOOKUP ===========================================\n";
256             unless (defined $xml) {
257             $store_mode='NO';
258              
259             my $names = join('', (keys %{$self->config_data('regressions')}));
260              
261             unless ($names eq '') {
262             my $stored = $self->config_data('regressions');
263              
264             #list reports to upload
265             print "Loading test runs that are ready for upload:\n";
266             $xml = _generate_signed_xml_from_struct($self, $credentials, $stored);
267             ########### Removed this on Jan 8th 2007, not needed !!!!
268             ########### #url encode the document
269             ########### $xml =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg;
270             ########### Removed this on Jan 8th 2007, not needed !!!!
271              
272             print "done.\n";
273             }
274             else {
275             print "No local test result found. You might want execute the tests first. Exiting.\n";
276             exit -1;
277             }
278             }
279              
280             print "== INTEGRATOR SERVER DATA UPLOADING ================================\n";
281             print "Sending data to server\n";
282             my $response = _harness_post_url($credentials, $xml);
283             print "done.\n";
284              
285             #look in the server response and update local stuff
286             if ($response->{_rc} eq "200") {
287             print "HTTP transfer done.\n";
288             print "Analysing results...\n";
289             my $xs = XML::Simple->new();
290             my %IN_options = (
291             NoAttr => 0,
292             ForceArray => 1,
293             KeyAttr => [],
294             );
295              
296             my $xml_server;
297             eval { $xml_server = $xs->XMLin($response->{_content}, %IN_options) };
298             if ($@) {
299             open STDOUT, ">server_response.err"
300             or die "Error: cannot write malformed xml error file: $!, $?, $@";
301             print "$!, $?, $@\nResponse from server was:\n";
302             print Dumper $response;
303             die "Error, problem loading confirmation from server "
304             ."(see file 'server_response.err'), malformed xml: $@";
305             }
306             my $persist = $self->config_data('regressions');
307             print "XML from server response is loaded.\n";
308              
309             print "== INTEGRATOR SERVER INFO ==========================================\n";
310             _info_warning_errors_feedback($xml_server);
311              
312             ###print Dumper $xml_server;
313              
314             print "== LOCAL DATABASE CLEAN-UP =========================================\n";
315             if (defined $xml_server->{integrator_response}[0]{integrator_global_return}[0]) {
316             my $resp = $xml_server->{integrator_response}[0];
317             my $global = $resp->{integrator_global_return}[0];
318              
319             if ($global eq 'success') {
320             print "\t...local data no longer needed, cleaning-up...\n";
321             foreach my $uuid (keys %{$persist}) {
322             delete( $persist->{$uuid});
323             }
324             $self->config_data('regressions' => $persist);
325             print "\t...local data cleaned-up.\n";
326             }
327             else {
328             print "Error: Integrator Global Return code is $global, cannot clean-up locally !\n";
329             print "see file 'server_response.err' for more details\n";
330             open STDOUT, ">server_response.err" or die "Error: cannot write server_log: $!";
331             print "$!, $?, $@\nResponse from server was:\n";
332             print Dumper $response;
333             }
334             }
335             else {
336             print "ERROR: Integrator did not provide a global return code, cannot clean-up locally !\n";
337             print "see file 'server_response.err' for more details\n";
338             open STDOUT, ">server_response.err" or die "Error: cannot write server_log: $!";
339             print "$!, $?, $@\nResponse from server was:\n";
340             print Dumper $response;
341             }
342             }
343             else {
344             print "HTTP Transfer problem...\n";
345             $response->{_rc} ||='';
346             $response->{msg} ||='';
347             print "Apache: return_code='$response->{_rc}', msg='$response->{_msg}'\n";
348             print "Apache: content: $response->{_content}'\n";
349             }
350             print "Performed sync in " .(time - $t). " sec.\n";
351             }
352              
353             =head2 ACTION_integrator_version
354              
355             used with: ./Build integrator_version
356              
357             Displays the Integrator::Module::Build version currently running.
358              
359             =cut
360            
361             sub ACTION_integrator_version {
362             print "This is Integrator::Build::Module version $Integrator::Module::Build::VERSION\n";
363             print << 'EOLIC';
364             Copyright 2007 Cydone Solutions Inc, all rights reserved.
365              
366             This program is free software; you can redistribute it and/or modify it
367             under the same terms as Perl itself.
368             EOLIC
369             }
370              
371             =head2 ACTION_version
372              
373             used with: ./Build version
374              
375             See integrator_version ...
376              
377             =cut
378              
379             #not documented but will also work...
380             sub ACTION_version {
381             ACTION_integrator_version();
382             }
383              
384              
385             =head2 ACTION_integrator_store
386              
387             used with: ./Build integrator_store
388              
389             This action is used as a patch to help load externally generated XML. This
390             is used only as a debugging mechanism...
391              
392             =cut
393              
394             sub ACTION_integrator_store {
395             my $self = shift;
396              
397             my @files = _get_valid_filenames($self->{args}{file});
398            
399             foreach my $file_in (@files) {
400             die "Error: could not read '$file_in', $?"
401             unless (-r $file_in);
402              
403             #list reports to upload
404             print "Loading xml report file from $file_in:\n";
405             open FIN, "<$file_in" or die "Error: could not open '$file_in', $?";
406             my $xml = join '', ();
407             close FIN or die "Error: could not close '$file_in', $?";
408             print "done.\n";
409             ACTION_integrator_sync($self, $xml);
410             }
411             }
412              
413             sub _info_warning_errors_feedback {
414             my $xml_server = shift;
415              
416             #XXX TODO: this error handling will explode in the case where ERROR is not a string
417             # but a HASH !!! In this case we need to explore the hash (in sub _print_feedback)
418             # to print what is contained within... On wayt of reproducing this: corrupt the xml !!!
419              
420             if (defined $xml_server->{integrator_response}[0]) {
421             _print_feedback('INFO', $xml_server->{integrator_response}[0]{info});
422             _print_feedback('WARNING', $xml_server->{integrator_response}[0]{warning});
423             _print_feedback('ERROR', $xml_server->{integrator_response}[0]{error});
424             _print_feedback('TIME', $xml_server->{integrator_response}[0]{elapsed_time});
425             }
426             else {
427             die "Error: server response does not contain a valid 'integrator_response' field.\n";
428             }
429             }
430              
431             sub _print_feedback {
432             my $tag = shift;
433             my $table = shift;
434              
435             foreach my $result (@{$table}) {
436             print "$tag:\t$result\n";
437             }
438             }
439              
440             =head2 ACTION_integrator_download_test_definition
441              
442             used with: ./Build integrator_download_test_definition
443             ./Build integrator_download_test_definition --file=complete_this.xml
444              
445             This action is used to download all test definition data from the current
446             project in Cydone Integrator.
447              
448             =cut
449              
450             sub ACTION_integrator_download_test_definition {
451             my $self = shift;
452             my $credentials = _get_integrator_credentials($self);
453             $credentials->{integrator_test_definition_post}{value} = 'api/downloadTestDefinition';
454             #$credentials->{integrator_test_definition_post}{value} = 'frontend_dev.php/api/downloadTestDefinition';
455             my $t = time;
456             my @files;
457             my $xml;
458              
459             #get the filenames of xml files to complete, or setup default download
460             if (defined $self->{args}{param_file}) {
461             @files = _get_valid_filenames($self->{args}{param_file});
462             }
463             else {
464             push(@files, '___DEFAULT___weird_file_name___');
465             print "Default download: all data.\n";
466             $xml = _get_default_xml($credentials);
467             }
468             foreach my $file (@files) {
469             #list file to upload
470             unless ($file eq '___DEFAULT___weird_file_name___') {
471             print "Loading xml file '$file' for upload:\n";
472             open FIN,"$file" or die "Error opening file: $!";
473             $xml = join('', ());
474             close FIN or die "Error closing file: $!";
475             print "done.\n";
476             }
477              
478             print "Contacting server: ... fake ping for now ...";
479             print "done.\n";
480            
481             print "Sending data to server\n";
482             my $response = _test_definition_post_url($credentials, $xml);
483             print "done.\n";
484            
485             _analyse_server_answer($self, $response);
486              
487             last if ($file eq '___DEFAULT___weird_file_name___');
488             }
489             print "Performed download in " .(time - $t). " sec.\n";
490             }
491              
492             # a template sub to fake a default xml file
493             sub _get_default_xml {
494             my $credentials = shift;
495             return << "EOT";
496            
497             1.0
498             1.0
499            
500             $credentials->{integrator_project_code}{value}
501            
502            
503             EOT
504             }
505              
506             =head2 ACTION_integrator_send_xml
507              
508             used with: ./Build integrator_send_xml
509             ./Build integrator_send_xml --file=commands_api.xml
510              
511             This action is used to send a an xml api file to Cydone Integrator.
512              
513             =cut
514              
515             sub ACTION_integrator_send_xml {
516             my $self = shift;
517             my $credentials = _get_integrator_credentials($self);
518             $credentials->{integrator_test_definition_post}{value} = 'api/doapi';
519            
520             my $t = time;
521             my @files;
522              
523             #get the filenames to upload
524             if (defined $self->{args}{file}) {
525             @files = _get_valid_filenames($self->{args}{file});
526             }
527             else {
528             my $default_file = 'server_answer.xml';
529             print "Using default file $default_file\n";
530             push (@files, $default_file);
531             }
532              
533             foreach my $file (@files) {
534             #list file to upload
535             print "Loading xml file '$file' for upload:\n";
536             open FIN,"$file" or die "Error opening file: $!";
537             my $xml = join('', ());
538             close FIN or die "Error closing file: $!";
539             print "done.\n";
540            
541             print "Contacting server: ... fake ping for now ...";
542             print "done.\n";
543            
544             print "Sending data to server\n";
545             my $response = _test_definition_post_url($credentials, $xml);
546             print "done.\n";
547            
548             _analyse_server_answer($self, $response);
549             }
550             print "Performed upload in " .(time - $t). " sec.\n";
551             }
552              
553             =head2 ACTION_integrator_upload_test_definition
554              
555             used with: ./Build integrator_upload_test_definition
556             ./Build integrator_upload_test_definition --file=definition.xml
557              
558             This action is used to send a test definition xml file to Cydone Integrator.
559              
560             =cut
561              
562             sub ACTION_integrator_upload_test_definition {
563             my $self = shift;
564             my $credentials = _get_integrator_credentials($self);
565             $credentials->{integrator_test_definition_post}{value} = 'api/uploadTestDefinition';
566             #$credentials->{integrator_test_definition_post}{value} = 'frontend_dev.php/api/uploadTestDefinition';
567            
568             my $t = time;
569             my @files;
570              
571             #get the filenames to upload
572             if (defined $self->{args}{file}) {
573             @files = _get_valid_filenames($self->{args}{file});
574             }
575             else {
576             my $default_file = 'server_answer.xml';
577             print "Using default file $default_file\n";
578             push (@files, $default_file);
579             }
580              
581             foreach my $file (@files) {
582             #list file to upload
583             print "Loading xml file '$file' for upload:\n";
584             open FIN,"$file" or die "Error opening file: $!";
585             my $xml = join('', ());
586             close FIN or die "Error closing file: $!";
587             print "done.\n";
588            
589             print "Contacting server: ... fake ping for now ...";
590             print "done.\n";
591            
592             print "Sending data to server\n";
593             my $response = _test_definition_post_url($credentials, $xml);
594             print "done.\n";
595            
596             _analyse_server_answer($self, $response);
597             }
598             print "Performed upload in " .(time - $t). " sec.\n";
599             }
600              
601             sub _analyse_server_answer {
602             my $self = shift;
603             my $response = shift;
604             my $xml_no_attr = 1;
605             my $file_out = $self->{args}{file_out} |= 'server_answer.xml';
606              
607             #patch to intercept xml folding request with a '--xml_no_attr=0' on the cmd line
608             if (defined $self->{args}{xml_no_attr}) {
609             $xml_no_attr = ($self->{args}{xml_no_attr} eq 0) ? 0 : 1;
610             }
611              
612             if ($response->{_rc} eq '200') {
613             print "HTTP transfer done.\n";
614              
615             open OUT, ">$file_out" or die "Error, cannot write file $file_out: $!";
616             print "Data saved in file $file_out\n";
617             print OUT $response->{_content};
618             close OUT or die "Error, cannot close file $file_out: $!";
619             }
620             else {
621             print "HTTP Transfer problem...\n";
622             $response->{_rc} ||='';
623             $response->{msg} ||='';
624             print "Apache: return_code='$response->{_rc}', msg='$response->{_msg}'\n";
625             print "Apache: content: $response->{_content}'\n";
626             }
627             }
628              
629             #valid means readable file names from a glob
630             sub _get_valid_filenames {
631             my $glob = shift;
632             my @files;
633            
634             #do something
635             @files = glob($glob);
636              
637             foreach my $file (@files) {
638             die "Error: file $file is not readable, $!"
639             unless (-r $file);
640             chomp($file);
641             }
642             return @files;
643             }
644              
645             =head2 ACTION_integrator_xml_report
646              
647             used with: ./Build integrator_xml_report
648              
649             This action is used to generate a signed xml representation of all the
650             test runs launched with the 'integrator_test' action since the last sync
651             to the server. This action *will not* modify the local data, so it can
652             be used as often as needed.
653              
654             To remain compatible with Cydone Integrator, this action requires that
655             the user provides some credential information. All of this data is
656             first read from the Build.PL configuration file or prompted from the
657             command-line if more information is required.
658              
659             =cut
660              
661             # This action sub, called as a ./Build parameter, is used to compile all log results from
662             # the _build/integrator file (under the integrator_test_log key) in one huge xml file
663             # using the Test::TAP::XML report tool
664             sub ACTION_integrator_xml_report {
665             my $self = shift;
666            
667             my $credentials = _get_integrator_credentials($self, qr{or_project_code|or_lab_code});
668             my $t = time;
669             my $xml = _generate_signed_xml_from_struct( $self, $credentials, $self->config_data('regressions'));
670            
671             open FOUT, ">", "report.xml" or die "Error: could not create report.xml file, $!, $?";
672             print FOUT $xml;
673             close FOUT or die "Error: could not close report.xml after creating, $!, $?";
674             print STDERR "Generated xtml report in file \"report.xml\". Took ". (time - $t). " sec.\n";
675             }
676             sub _remove_undefs {
677             my $struct = shift;
678              
679             foreach my $key (keys %{$struct}) {
680             if (ref $struct->{$key} eq 'HASH') {
681             $struct->{$key} = _remove_undefs($struct->{$key});
682             }
683             elsif (ref $struct->{$key} eq 'ARRAY') { #an object of some sort
684             #XXX TO DO !!! we do nothing... for now... but we could look for undefs inside...
685             }
686             elsif ((ref $struct->{$key}) =~ /^Module.*Version$/) { #an object of some sort
687             $struct->{$key} = $struct->{$key}{original};
688             }
689             unless (ref $struct->{$key}) {
690             $struct->{$key} = '_UNDEF_' unless defined $struct->{$key};
691             }
692             }
693             return $struct;
694             }
695            
696             #This sub extracts runs from permanent data struct
697             sub _load_runs {
698             my $struct = shift;
699            
700             my @all_runs;
701             foreach my $uuid (keys %$struct) {
702             my $date = localtime($struct->{$uuid}{start_time});
703             print "\t...loading test run from user $struct->{$uuid}{integrator_user} ($uuid) "
704             ."dated from $date, for xml conversion.\n";
705            
706             my $run = Test::TAP::Model->new_with_struct( $struct->{$uuid} );
707             $run->{meat}{integrator_test_run_uuid} = $uuid;
708             push(@all_runs, $run->{meat} );
709             }
710             return @all_runs;
711             }
712              
713             sub _load_info {
714             my $self = shift;
715              
716             #we need to do some clean-up here also...
717             #to avoid use of uninitialised values, we patch the undef value into ''
718             #for performance reasons, we don't want to traverse the whole structure
719             #here, but the risk is that some new fields could create the problem again...
720             my $hi = $self->config_data('integrator_harness_info');
721             $hi->{script_files} ||= '';
722              
723             return $hi;
724             }
725              
726             # this sub returns the MD5 checksums for all files in directory 't'
727             # and will complain if a test file changed compared to the
728             # signature in the _build/integrator directory
729             sub _compute_test_signatures {
730             my $self = shift;
731             my $sign_values;
732              
733             #for all files under ./t
734             foreach my $file_name (sort @{$self->rscan_dir('t', qr{.*})} ) {
735             next if (-d $file_name); #skip directory entries
736             next if ($file_name =~ /\/CVS\//); #skip CVS files...
737              
738             #compute the signature
739             open SIGN, "$file_name" or die "Error, cannot read file $file_name for MD5 computation, $?";
740             my $file = join('',);
741             close SIGN or die "Error, cannot close file $file_name after MD5 computation, $?";
742             push(@$sign_values, {
743             'filename' => $file_name,
744             'md5' => md5_hex($file),
745             #'content' => $file, # we don't send the file... but we could...
746             } );
747            
748             }
749             my $date = localtime;
750             my $sign = {
751             'sign_date' => $date,
752             'sign_values' => $sign_values,
753             };
754             return $sign;
755             }
756              
757             #the output is a string containing the xml file
758             sub _generate_signed_xml_from_struct {
759             my $self = shift;
760             my $cred = shift;
761             my $struct = shift;
762             my $p = $self->{args};
763             my $xml_no_attr = 1;
764              
765             #patch to intercept xml folding request with a '--xml_no_attr=0' on the cmd line
766             if (defined $self->{args}{xml_no_attr}) {
767             $xml_no_attr = ($self->{args}{xml_no_attr} eq 0) ? 0 : 1;
768             }
769              
770             my @all_runs = _load_runs($struct);
771             my $info = _load_info($self);
772              
773             $info = _remove_undefs($info);
774              
775             my $harness_uuid = _get_uuid();
776              
777             #put a frame around it all
778             my $skel = {
779             'version' => '1.0',
780             'standalone' => '1',
781             'encoding' => 'UTF-8',
782             integrator_version => {
783             'version' => '1.0', # XXX change key for 'format_version'
784             },
785             integrator_security => {
786             'project_code' => $cred->{integrator_project_code}{value},
787             },
788             integrator_question => {
789             integrator_action => 'uploadTestResult',
790             },
791             integrator_data => {
792             'test_run' => \@all_runs,
793             'integrator_test' => {
794             'lab_code' => $cred->{integrator_lab_code}{value},
795             'harness_uuid' => $harness_uuid,
796             'signature' => '_TBD_MD5_HASH_',
797             # 'version' => '1.0', # XXX change key for 'format_version'
798             },
799             'integrator_harness_info' => $info,
800             }
801             };
802             my %OUT_options = (
803             RootName => 'xml',
804             NoAttr => $xml_no_attr,
805             KeyAttr => [],
806             ValueAttr => [],
807             );
808              
809             #serialise into xml
810             my $xs = XML::Simple->new();
811             #XXX put this in an eval block...
812             my $xml;
813              
814             # iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiio
815             #print Dumper $skel;
816             #exit;
817              
818             eval { $xml = $xs->XMLout($skel, %OUT_options) };
819             if ($@) {
820             die "Error, problem loading malformed xml: $@";
821             }
822            
823             # sign the document
824             my $sum = md5_hex($xml);
825             $xml =~ s/_TBD_MD5_HASH_/$sum/;
826            
827             return $xml;
828             }
829              
830             sub _print_table_header {
831             my $user = shift;
832             my $date = shift;
833             return <<"EOT";
834            
835             Test case run by $user, started on $date
836            
837             EOT
838             }
839              
840             sub _get_integrator_credentials {
841             my $self = shift;
842             my $filter = shift; #optional parameter to filter-in credentials to use
843             my $p = $self->{properties};
844             my $cred = {
845             integrator_project_code => {
846             default => 'my_project',
847             question => 'a project code',
848             order => 10,
849             regexp => qr{\w},
850             },
851             integrator_lab_code => {
852             default => 'first',
853             question => 'a lab id',
854             order => 15,
855             regexp => qr{\w},
856             },
857             integrator_user => {
858             default => 'nobody',
859             question => 'a username for the Integrator login',
860             order => 20,
861             regexp => qr{\w},
862             },
863             integrator_pwd => {
864             default => 'nopwd',
865             question => 'an api password for the Integrator login',
866             order => 30,
867             regexp => qr{\w},
868            
869             no_echo => 1,
870             },
871             integrator_url => {
872             default => 'https://public1.partnerscydone.com',
873             question => 'an Integrator web url',
874             order => 40,
875             regexp => qr{\w},
876             },
877             integrator_http_realm => {
878             default => 'Integrator Demo',
879             question => 'an Integrator realm for the web page authentication',
880             order => 60,
881             regexp => qr{\w},
882             },
883             integrator_http_user => {
884             default => 'demo',
885             question => 'an http user name',
886             order => 70,
887             regexp => qr{\w},
888             },
889             integrator_http_pwd => {
890             default => 'okok',
891             question => 'an http password',
892             order => 80,
893             regexp => qr{\w},
894              
895             no_echo => 1,
896             },
897             };
898              
899             #ask for values until match with regexp
900             foreach my $key ( sort {$cred->{$a}{order} <=> $cred->{$b}{order}} keys %$cred ) {
901             #filter-out non matching tags
902             next if ((defined $filter) and ($key !~ $filter));
903            
904             #copy the value from the parameters, then fill-in the blanks
905             $cred->{$key}{value} = $p->{$key} ||='';
906             while ($cred->{$key}{value} !~ $cred->{$key}{regexp}) {
907             print "\nThe value of '$cred->{$key}{value}' for parameter '$key' in Build.PL. is not valid.\n";
908             print "\tPlease enter $cred->{$key}{question} "
909             ."(or press ENTER for default '$cred->{$key}{default}')\n";
910            
911             ReadMode( ($cred->{$key}{no_echo}) ? 'noecho' : 'restore' );
912             my $answer = ;
913             ReadMode('restore');
914            
915             chomp($answer);
916             print "\n" if ($cred->{$key}{no_echo});
917            
918             $cred->{$key}{value} = ($answer =~ /^$/) ? $cred->{$key}{default} : $answer;
919             }
920             print "Credentials: '$key'\tis set to: $cred->{$key}{value}\n" unless ($cred->{$key}{no_echo});
921             }
922              
923             return $cred;
924             }
925              
926             sub _harness_post_url {
927             my $cred = shift;
928             my $xml = shift;
929              
930             my $browser = LWP::UserAgent->new;
931              
932             # Create a user agent object
933             $browser->agent("Integrator::Module::Build/$Integrator::Module::Build::VERSION");
934              
935             #this parameter has a default value and is no longer required from the user...
936             $cred->{integrator_sync_page}{value} |= 'api/doapi';
937            
938             #clean-up the urls
939             $cred->{integrator_url}{value} =~ s/\/$//g;
940             $cred->{integrator_sync_page}{value} =~ s/\/$//g;
941             $cred->{integrator_sync_page}{value} =~ s/^\///g;
942             $cred->{complete_url}{value} = $cred->{integrator_url}{value} .'/'. $cred->{integrator_sync_page}{value};
943              
944             if($cred->{integrator_http_realm}) {
945             $browser->credentials(URI->new($cred->{integrator_url}{value})->host_port, #yark !
946             $cred->{integrator_http_realm}{value},
947             $cred->{integrator_http_user}{value} => $cred->{integrator_http_pwd}{value});
948             }
949              
950             # Pass request to the user agent and get a response back
951             my $res = $browser->post( $cred->{complete_url}{value},
952             [
953             'query' => $xml,
954             'user_name' => $cred->{integrator_user}{value},
955             'password' => $cred->{integrator_pwd}{value}
956             ]);
957             # Check the status of the response
958             unless ($res->is_success) {
959             print $res->status_line, "\n";
960             }
961             return $res;
962             }
963              
964             sub _test_definition_post_url {
965             my $cred = shift;
966             my $xml = shift;
967              
968             my $browser = LWP::UserAgent->new;
969              
970             # Create a user agent object
971             $browser->agent("Integrator::Module::Build/$Integrator::Module::Build::VERSION");
972              
973             #clean-up the urls
974             $cred->{integrator_url}{value} =~ s/\/$//g;
975             $cred->{integrator_test_definition_post}{value} =~ s/\/$//g;
976             $cred->{integrator_test_definition_post}{value} =~ s/^\///g;
977             $cred->{complete_url}{value} = $cred->{integrator_url}{value}
978             .'/'. $cred->{integrator_test_definition_post}{value};
979              
980             if($cred->{integrator_http_realm}) {
981             $browser->credentials(URI->new($cred->{integrator_url}{value})->host_port, #yark !
982             $cred->{integrator_http_realm}{value},
983             $cred->{integrator_http_user}{value} => $cred->{integrator_http_pwd}{value});
984             }
985              
986             # Pass request to the user agent and get a response back
987             my $res = $browser->post( $cred->{complete_url}{value},
988             [
989             'query' => $xml,
990             'user_name' => $cred->{integrator_user}{value},
991             'password' => $cred->{integrator_pwd}{value}
992             ]);
993              
994             # Check the outcome of the transaction
995             unless ($res->is_success) {
996             print $res->status_line, "\n";
997             }
998             return $res;
999             }
1000              
1001             sub _log_results_with_look_up {
1002             my $IMB = shift;
1003             my $t = shift;
1004             my $NL = $NEW_LINE;
1005             my $SHRP= $POUND_SIGN;
1006              
1007             #load persistant regression object
1008             my $reg_log = $IMB->config_data('regressions');
1009            
1010             #update regression data with a unique key, and other fields
1011             my $uuid = _get_uuid();
1012             $reg_log->{$uuid} = $t->structure;
1013             $reg_log->{$uuid}{integrator_status} = 'NEW_INSERTION';
1014             $reg_log->{$uuid}{ran_by} = "$ENV{USER}";
1015              
1016             #patch to assign a default value to the 'pos' fields, they appear as undef
1017             #if left untouched and it throws warnings under Test::TAP::Model reload...
1018             #Pos means 'position' and is related to 'pugs', a perl6 simulator ???
1019             ######### From Test::TAP::Model pod page #################################
1020             # pugs auxillery stuff, from the comment
1021             # pos => # the place in the test file the case is in
1022             ##########################################################################
1023             # multiple line entries are also "encoded"...
1024             foreach my $luuid (keys %{$reg_log}) {
1025             foreach my $run ( $reg_log->{$luuid}{test_files} ) {
1026             foreach my $file (@$run) {
1027             #line feed replacements
1028             $file->{pre_diag} =~ s/\n/$NL/g if (defined $file->{pre_diag} );
1029             $file->{pre_diag} =~ s/#/$SHRP/g if (defined $file->{pre_diag} );
1030             foreach my $event ($file->{events}) {
1031             foreach my $step (@$event) {
1032             #measurement look-up, default values and line feed replacements
1033             _extract_measurement ($step);
1034             _extract_configuration($step);
1035             _extract_config_file ($step);
1036             $step->{pos} ||= '';
1037             $step->{line} =~ s/\n/$NL/g if (defined $step->{line} );
1038             $step->{line} =~ s/#/$SHRP/g if (defined $step->{line} );
1039             $step->{diag} =~ s/\n/$NL/g if (defined $step->{diag} );
1040             $step->{diag} =~ s/#/$SHRP/g if (defined $step->{diag} );
1041             }
1042             }
1043             }
1044             }
1045             }
1046              
1047             #commit the data
1048             $IMB->config_data("regressions" => $reg_log);
1049             }
1050              
1051             #this internal function is used to extract measurement declarations from the TAP output
1052             #and cast it in the attribute values of the event tag. Measurements are thus considered
1053             #to be a special sort of events.
1054             sub _extract_measurement {
1055             my $step = shift;
1056              
1057             #split on ; and assign default value of ''.
1058             if ($step->{line} =~ /integrator_measurement:\s*(.*)/) {
1059             my $line = $1;
1060             chomp($line);
1061             $line .= ';;;;;'; #patch to have map do its thing on all fields.
1062             ( $step->{integrator_meas_name} ,
1063             $step->{integrator_meas_value} ,
1064             $step->{integrator_meas_unit} ,
1065             $step->{integrator_meas_tol} ,
1066             $step->{integrator_meas_equip} ) = split (/\s*;\s*/,$line);
1067             }
1068             foreach my $key (keys %$step) {
1069             $step->{$key} ||= '';
1070             }
1071             }
1072              
1073             #this internal function is used to extract confirguration data that
1074             #was declared from the TAP output.
1075             sub _extract_configuration {
1076             my $step = shift;
1077              
1078             #split on ; and assign default value of ''.
1079             if ($step->{line} =~ /integrator_component:\s*(.*)/) {
1080             my $line = $1;
1081             chomp($line);
1082             $line .= ';;;;;;';
1083             ( $step->{integrator_cmp_name} ,
1084             $step->{integrator_cmp_serial} ,
1085             $step->{integrator_cmp_state_name} ,
1086             $step->{integrator_cmp_state_value} ) = split (/\s*;\s*/, $line);
1087             }
1088             foreach my $key (keys %$step) {
1089             $step->{$key} ||= '';
1090             }
1091             }
1092              
1093             #this internal function is used to extract config files that
1094             #are extracted from the TAP output.
1095             sub _extract_config_file {
1096             my $step = shift;
1097              
1098             if (defined $step->{diag}
1099             and $step->{diag} =~ /integrator_config_data:\s*(.*)\s*;\s*(.*)\s*;(.*)\s*integrator_config_data_end:/s) {
1100            
1101             my $file_name = $1;
1102             my $size = $2;
1103             my $content = $3;
1104             chomp($file_name);
1105              
1106             $content =~ s/\#\s+//gm;
1107             $content =~ s/\n*//gm;
1108             $step->{diag} = "integrator_config_data: $file_name";
1109             push (@{$step->{integrator_config_file}}, { file_name => $file_name,
1110             file_size => $size,
1111             file_content => $content } );
1112             }
1113             }
1114              
1115             sub _load_xml {
1116             my $file = shift;
1117             my $struct;
1118              
1119             my $xs = XML::Simple->new();
1120             my %IN_options = (
1121             NoAttr => 0,
1122             ForceArray => 1,
1123             KeyAttr => [],
1124             );
1125              
1126             eval { $struct = $xs->XMLin($file ,%IN_options) };
1127             if ($@) {
1128             die "Error, problem loading malformed xml: $@";
1129             }
1130              
1131             return $struct;
1132             }
1133              
1134             sub _file_contains_text {
1135             my $file = shift;
1136             my $text = shift;
1137              
1138             if (-r $file) {
1139             open FIN, "<$file" or die "Error, cannot open file $file for inspection, $?";
1140             my $content = join ('', );
1141             close FIN or die "Error, cannot close file $file after inspection, $?";
1142            
1143             return 1 if ($content =~ /$text/);
1144             return 0;
1145             }
1146             #file is not readable, assume no.
1147             else {
1148             return 0;
1149             }
1150             }
1151              
1152             #function taken from the O'Reilly cookbook.
1153             sub _is_rw_safe {
1154             my $path = shift;
1155             my $info = stat($path);
1156             return unless $info;
1157              
1158             # owner neither superuser nor me
1159             # the real uid is stored in the $< variable
1160             if (($info->uid != 0) && ($info->uid != $<)) {
1161             return 0;
1162             }
1163              
1164             # check whether group or other can write file.
1165             # use 066 to detect either reading or writing
1166             if ($info->mode & 066) { # someone else can write this
1167             return 0 unless -d _; # non-directories aren't safe
1168             # but directories with the sticky bit (01000) are
1169             return 0 unless $info->mode & 01000;
1170             }
1171              
1172             return 1;
1173             }
1174              
1175             #crude... but portable
1176             sub _get_uuid {
1177             return md5_hex(time + rand(10000));
1178             }
1179              
1180             =head1 AUTHOR
1181              
1182             Francois Perron, Cydone Solutions Inc.
1183              
1184             =head1 BUGS
1185              
1186             Please report any bugs or feature requests to
1187             C, or through the web interface at
1188             L.
1189             I will be notified, and then you'll automatically be notified of progress on
1190             your bug as I make changes.
1191              
1192             =head1 SUPPORT
1193              
1194             You can find documentation for this module with the perldoc or man commands.
1195              
1196             perldoc Integrator::Module::Build
1197             man Integrator::Module::Build
1198              
1199             You can also look for information at: L or L
1200              
1201             =head1 ACKNOWLEDGEMENTS
1202              
1203             This module would not have been possible without the great contributions
1204             by Ken Williams, Andy Lester, chromatic, Michael G Schwern and all folks
1205             involved in the creation of Test::... , Module::Build, Module::Starter
1206             and supporting modules.
1207              
1208             =head1 COPYRIGHT & LICENSE
1209              
1210             Copyright 2007 Cydone Solutions Inc, all rights reserved.
1211              
1212             This program is free software; you can redistribute it and/or modify it
1213             under the same terms as Perl itself.
1214              
1215             =cut
1216              
1217             1; # End of Integrator::Module::Build