=head1 NAME Test::Metadata - Class for capturing build and test log data and generating an XML Metadata file about it. =head1 SYNOPSIS use Test::Metadata; use Test::Parser::KernelBuild; use Test::Parser::MyTest; my $build_results = new Test::Parser::KernelBuild; $build_results->parse("kernel_build.log"); my $test_results = new Test::Parser::MyTest; $test_results->parse("my_test.log"); my $metadata = new Test::Metadata; $metadata->add_build($build_results); $metadata->add_test($test_results); print $metadata->to_xml(); =head1 DESCRIPTION This module provides an interface for creating metadata summaries of software build and test results. It is designed to work in conjunction with Test::Parser subclasses, to allow you to gather and report on a set of tests run atop a given set of built software. Essentially, you use Test::Parser to parse all your build and test files, and then add each of them to a Test::Metadata object. You can then generate an XML file for the combined results, suitable for use with other tools. The XML format used is the 'Test Result Publication Interface' (TRPI) XML schema, developed by SpikeSource. See http://www.spikesource.com/testresults/index.jsp?show=trpi-schema =head1 FUNCTIONS =cut package Test::Metadata; use strict; use warnings; use Test::Parser; use XML::Twig; use fields qw( id suite_type total_executed total_passed total_failed total_skipped builds results properties _RETAINED_XML ); use vars qw( %FIELDS $VERSION ); our $VERSION = '1.3'; =head2 new() Creates a new Test::Metadata object. =cut sub new { my $this = shift; my $class = ref($this) || $this; my $self = bless [\%FIELDS], $class; $self->{properties} = { description => "", summary => "Test results", license => "GPL", product => "", version => "", url => "", root => "", coverage_percent => "", coverage_report_path => "", }; $self->{builds} = []; $self->{results} = []; $self->{total_executed} = 0; $self->{total_passed} = 0; $self->{total_failed} = 0; $self->{total_skipped} = 0; return $self; } =head3 add_build($build) Takes a Test::Parser object and extracts information about it and builds an internal build metadata representation. =cut sub add_build { my $self = shift; my $build = shift or return undef; my $build_metadata = { 'name' => $build->name(), 'path' => $build->path(), 'build_status' => $build->errors()==0? 'pass' : 'fail', }; push @{$self->{builds}}, $build_metadata; } sub add_test { my $self = shift; my $test = shift or return undef; # TODO: Need a better way to specify the expected values # TODO: Need a way to specify reports my $test_metadata = { 'name' => $test->name(), 'path' => $test->path(), 'suite_type' => $test->type(), 'num_executed' => $test->num_executed(), 'num_passed' => $test->num_passed(), 'num_failed' => $test->num_failed(), 'num_skipped' => $test->num_skipped(), 'expect_executed' => $test->num_executed(), 'expect_passed' => $test->num_executed(), 'expect_failed' => 0, 'expect_skipped' => 0, 'report_name' => "", 'report_path' => "", }; push @{$self->{results}}, $test_metadata; } sub total_executed { my $self = shift; my $total_executed = 0; foreach my $test (@{$self->{results}}) { $total_executed += $test->{num_executed}; } return $total_executed; } sub total_passed { my $self = shift; my $total = 0; foreach my $test (@{$self->{results}}) { $total += $test->{num_passed}; } return $total; } sub total_failed { my $self = shift; my $total = 0; foreach my $test (@{$self->{results}}) { $total += $test->{num_failed}; } return $total; } sub total_skipped { my $self = shift; my $total = 0; foreach my $test (@{$self->{results}}) { $total += $test->{num_skipped}; } return $total; } sub properties { my $self = shift; my $properties = shift; if ($properties and ref($properties) eq 'HASH') { $self->{properties} = $properties; } return $self->{properties}; } =head2 substitute_template($template, $data_hashref) Performs substitutions of values in the form '[%variable%]' with the corresponding value given by $data_hashref->{variable}. Any undefined items are replaced with blank strings. =cut sub substitute_template { my $self = shift; my ($text, $data) = @_; foreach my $key (keys %{$data}) { my $value = $data->{$key} || ''; $text =~ s/\[%\s*$key\s*%\]/$value/g; } $text =~ s/\[%\s*\w+\s*%\]//g; return $text; } =head2 to_xml() Returns the parsed test result data in the TRPI XML syntax (http://www.spikesource.com/testresults/index.jsp?show=trpi-schema). In the case of an error, undef will be returned. The error message can be retrieved via error(). =cut sub to_xml { my $self = shift; my $xml = qq| [%description%] [%summary%] [%license%] [%vendor%] [%release%] [%url%] [%root%] [%platform%] |; # If build logs available, substitute the build info if (defined($self->{builds}) and @{$self->{builds}} > 0) { my $build_xml = ''; my $build_status = 'pass'; foreach my $build (@{$self->{builds}}) { my $text = qq| \n|; warn "Substituting build info\n"; $build_xml .= $self->substitute_template($text, $build); if ($build_status eq 'pass' && defined $build->{build_status}) { $build_status = $build->{build_status}; } } if ($build_xml) { $xml .= qq| \n|; $xml .= $build_xml; $xml .= qq| \n\n|; } } # Substitute all the results foreach my $result (@{$self->{results}}) { my $result_xml = qq| |; warn "Substituting a result\n"; $xml .= $self->substitute_template($result_xml, $result); } $xml .= qq| |; warn "Substituting properties...\n"; return $self->substitute_template($xml, $self->{properties}); } =head2 parse($file) Parses the given xml (TRPI) file or url and loads the data into the current object. =cut sub parse { my $self = shift; my $file = shift; # TODO: Parse XML my $twig = XML::Twig->new( map_xmlns => { 'http://www.spikesource.com/xsd/2005/04/TRPI' => 'trpi' }, pretty_print => 'indented', comments => 'keep', pi => 'keep', keep_original_prefix => 1 ); if ($file =~ m/n.*\n/ || (ref $file eq 'IO::Handle')) { eval { $twig->parse($file); }; if ($@) { $self->{_errormsg} = "XML::Twig died; this may mean invalid XML: $@\n"; return undef; } } elsif ($file =~ /^http/ or $file =~ /^ftp/) { eval { $twig->parseurl($file); }; if ($@) { $self->{_errormsg} = "XML::Twig died; this may mean invalid XML: $@\n"; return undef; } } elsif (! -e $file) { $self->{_errormsg} = "No such file '$file'\n"; return undef; } else { eval { $twig->parsefile($file); }; if ($@) { $self->{_errormsg} = "XML::Twig died; this may mean invalid XML: $@\n"; return undef; } } if (not ref $twig) { $self->{_errormsg} = "XML::Twig did not return a valid XML object"; return undef; } # TODO: Load data into structure my $component = $twig->root()->first_descendant('component'); if (not ref $component) { $self->{_errormsg} = "No 'component' element found in document"; return undef; } $self->{id} = ''; $self->{suite_type} = ''; $self->{total_executed} = ''; $self->{total_passed} = ''; $self->{total_failed} = ''; $self->{builds} = (); $self->{results} = (); $self->{properties} = {}; # TODO: Extract items from the XML return 1; } =head2 id() Returns the id of the parsed test. =head2 total_executed Returns the total number of executed test cases =head2 total_passed Returns the total number of passed test cases =head2 total_failed Returns the total number of failed test cases =head2 total_skipped Returns the total number of test cases that were not run =head1 PREREQUISITES None =head1 AUTHOR Bryce Harrington =head1 COPYRIGHT Copyright (C) 2005 Bryce Harrington. All Rights Reserved. This script is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 SEE ALSO L, L =cut 1;