=head1 expectedFail.pm Documentation for expectedFail.pm =head1 Overview The module expectedFail.pm supplies the capability of checking if a failed test is expected to fail. It is called directly from either test_driver.sh (for batch and interactive tests) or build-namelist_test.pl. Future plans involve integrating this module into cesm tests. =head1 Use Case This is a new feature being added to the existing CLM test infrastructure. The use case would roughly be along the lines of: 1) Run the test suite (CLM batch,interactive or namelist) 2) Search for test failures a) Fix failed tests b) -or- Add new xFail entries to XML file if a test is supposed to fail (eg. due to some missing resolution). 3) Check for new tests that now pass. This is for modifying the ChangeLog. 4) Update XML file by either adding new entries or removing old ones. 5) update the ChangeLog to reflect important changes in test behavior (Tests that now pass that failed before, tests that are now xFail, etc... =head2 Public methods There are two public methods needed. The "new" ctor and one of the parseOutput* methods. Everything else is private. xFail::expectedFail->new parseOutput parseOutputCLM =head2 Private methods sub _searchExpectedFail sub _readXml sub _testNowPassing sub _printOutput sub _getTestType sub _getMachInfo =cut package xFail::expectedFail; our $VERSION = '1.00'; use Cwd; use strict; use Getopt::Long; use English; use Scalar::Util qw(looks_like_number); my @testList={}; my $DEBUG=0; my $pass=" PASS"; my $fail=" FAIL"; my $xfail="xFAIL"; ############################################################################## # ############################################################################## =head1 CTOR Constructor for the class. Reads in three arguments: _callingName -> name of the script creating the new object _compareGenerate -> compare or generate option _totTests -> total number of tests to run Calls _readXml which reads the file expectedClmTestFails.xml and stores it memory for later searches. returns: new object ($self) =cut ############################################################################## # ############################################################################## sub new { my ($class_name) = @_; my $self = { _className => shift, _callingName => shift, _compareGenerate => shift, _totTests => shift, _foundList => undef, _numericalTestId => undef }; if ($DEBUG) { print "$self->{_callingName}\n"; print "$self->{_compareGenerate}\n"; } bless ($self, $class_name); $self->{_numericalTestId}=0; $self->{_created} = 1; $self->_readXml(); return $self; } ############################################################################## # ############################################################################## =head1 parseOutput parseOutput parsese the output from the build-namelist_test.pl script. It is similar to, but not interchangable with parseOutputCLM. The only argument is that of the reference variable that contains the information dumped by Test::More. returns: nothing =cut ############################################################################## # ############################################################################## sub parseOutput { my $report; my $testId; my @testName={}; my $testReason; my ($self, $output) = @_ ; #_#=========================================== #_# keep this in for logging #_#=========================================== print ("captured output is :: \n $output \n"); #_# split the output from Test::More output on newline my @refList = split('\n', $output); #_# process any buffered output which happens when a subroutine from build-namelist_test.pl #_# itself calls some testing routines foreach my $refSplit (@refList) { #_# always look at the last element of refSplit since that will have the info. from the #_# last test run my @outArr=split(/ /,$refSplit); if ($DEBUG) { print ("\nxFail::expectedFail::parseOutput @outArr[0] \n"); print ("xFail::expectedFail::parseOutput @outArr[1] \n"); print ("xFail::expectedFail::parseOutput @outArr[2] \n"); print ("xFail::expectedFail::parseOutput @outArr[3] \n"); print ("xFail::expectedFail::parseOutput @outArr[4] \n"); } my $size = @outArr-1; #_# first case, we have a passed (ok) test if (@outArr[0] eq "ok") { $self->{_numericalTestId}++; $report=$pass; $testId=@outArr[1]; @testName=@outArr[3..$size]; $testReason=""; my ($retVal,$xFailText)=$self->_searchExpectedFail($testId); my $testReason=$self->_testNowPassing($testId,$retVal,$xFailText); if($DEBUG){ print("$testReason \n"); } $self->_printOutput($report,$testId,$testReason,@testName); #_# deal with the case of a failed (not ok) test } elsif (@outArr[0] eq "not") { $self->{_numericalTestId}++; $testId=@outArr[2]; my ($retVal,$xFailText)=$self->_searchExpectedFail($testId); if ($DEBUG) { print ("xFail::expectedFail::parseOutput Id $retVal,$xFailText \n"); } @testName=@outArr[4..$size]; if ($retVal eq "TRUE"){ #_# found an expected FAIL (xFAIL) $report=$xfail; $testReason= ""; } else { #_# print a regular FAIL $report=$fail; $testReason=""; } $self->_printOutput($report,$testId,$testReason,@testName); } else { #_# skipping line. Trying to parse error code from Test::More } } #_# this resets the reference that points to $output (\$captOut) on the caller side @_[1]=""; } ############################################################################## # ############################################################################## =head1 parseOutputCLM parseOutputCLM parsese the output from the test_driver.sh script. It is similar to, but not interchangable with parseOutput. parseOutputCLM takes one arguments: $statFoo-> the name of the td..status file returns: nothing =cut ############################################################################## # ############################################################################## sub parseOutputCLM { my $report; my $testId; my @testName={}; my $testReason; my ($self, $statFoo) = @_ ; open(FOO, "< $statFoo"); # open for input open(FOO_OUT, "> $statFoo.xFail"); # open for input my(@reportLines); while () { my($line) = $_; my @outArr=split(/ /,$line); if (looks_like_number(@outArr[0])) { $self->{_numericalTestId}++; my $num=sprintf("%03d", $self->{_numericalTestId}); my $totNum=sprintf("%03d", $self->{_totTests}); #_# last element has the pass/fail info. chomp(@outArr[-1]); my $repPass=substr(@outArr[-1], -4, 4); if ($DEBUG) { print ("xFail::expectedFail::parseOutput @outArr[0] \n"); print ("xFail::expectedFail::parseOutput @outArr[1] \n"); print ("xFail::expectedFail::parseOutput @outArr[2] \n"); print ("xFail::expectedFail::parseOutput @outArr[3] \n"); print ("xFail::expectedFail::parseOutput @outArr[4] \n"); print ("xFail::expectedFail::parseOutput @outArr[5] \n"); print ("xFail::expectedFail::parseOutput @outArr[6] \n"); print ("xFail::expectedFail::parseOutput @outArr[-1] \n"); print ("xFail::expectedFail::parseOutput $repPass \n"); } my $size = @outArr-1; if ($DEBUG) { print ("size of line $size \n"); } my $endOfDesc=$size-1; if ($repPass eq "PASS") { $report=$pass; $testId=@outArr[1]; @testName=@outArr[2..$endOfDesc]; my ($retVal,$xFailText)=$self->_searchExpectedFail($testId); my $testReason=$self->_testNowPassing($testId,$retVal,$xFailText); #_# print out the test results print FOO_OUT ("$num/$totNum <$report> $testReason \n"); } else { $testId=@outArr[1]; my ($retVal,$xFailText)=$self->_searchExpectedFail($testId); if ($DEBUG) { print ("xFail::expectedFail::parseOutput Id $retVal,$xFailText \n"); } @testName=@outArr[2..$endOfDesc]; if ($retVal eq "TRUE"){ #_# found an expected FAIL (xFAIL) $report=$xfail; $testReason= ""; } else { #_# print a regular FAIL $report=$fail; $testReason=""; } #_# print out the test results print FOO_OUT ("$num/$totNum <$report> $testReason \n"); } } else { print FOO_OUT $line; } } close(FOO); close(FOO_OUT); } ############################################################################## # ############################################################################## =head1 _searchExpectedFail searches the list of expected fails for a match with testId. _searchExpectedFail takes one arguments: $testId-> the test id (numerical or string) that we want to search for returns: $retVal (TRUE or FALSE) if id was found $text text from XML file =cut ############################################################################## # ############################################################################## sub _searchExpectedFail { my ( $self,$testId) = @_; #search through list for test ID my $retVal="FALSE"; if ($DEBUG) { print ("here 2 Id $self->{_foundList} \n"); } if ($self->{_foundList} eq "FALSE"){ if ($DEBUG) { print ("returning early Id \n"); } return $retVal; } my $failType; my $text; foreach my $tL (@testList) { my %tAtts = $tL->get_attributes(); my $tid=$tAtts{'testId'}; if ($DEBUG) { print ("_seachExpectedFail Id $tid $testId \n"); } if ($tid eq $testId) { if ($DEBUG) { print ("here Id \n"); } #~# found the test we're looking for $text=$tL->get_text(); $failType=$tAtts{'failType'}; if ($failType eq "xFail"){ $retVal="TRUE"; } } } return ($retVal,$text); } ############################################################################## # ############################################################################## =head1 _readXml reads the xml file for a particular machine, compiler, test type and (compare | generate) setup and saves it in memory for searching by _searchExpectedFail. _readXml takes no arguments returns: nothing =cut ############################################################################## # ############################################################################## sub _readXml { my ( $self ) = @_; #Figure out where configure directory is and where can use the XML/Lite module from my $ProgName; ($ProgName = $PROGRAM_NAME) =~ s!(.*)/!!; # name of program my $ProgDir = $1; # name of directory where program lives my $cwd = getcwd(); # current working directory my $cfgdir; if ($ProgDir) { $cfgdir = $ProgDir; } else { $cfgdir = $cwd; } #----------------------------------------------------------------------------------------------- # Add $cfgdir to the list of paths that Perl searches for modules my @dirs = ( $cfgdir, "$cfgdir/perl5lib", "$cfgdir/../../cime/utils/perl5lib", "$cfgdir/../../../cime/utils/perl5lib" ); unshift @INC, @dirs; my $result = eval "require XML::Lite"; if ( ! defined($result) ) { die <<"EOF"; ** Cannot find perl module \"XML/Lite.pm\" from directories: @dirs ** EOF } #----------------------------------------------------------------------------------------------- my ($machine,$compiler)=_getMachInfo(); my $testType=$self->_getTestType($self->{_callingName}); my $xmlFile=undef; if ($testType eq "clmInteractive" || $testType eq "clmBatch") { $xmlFile = "$cfgdir/expectedClmTestFails.xml"; } elsif ($testType eq "namelistTest") { $xmlFile = "xFail/expectedClmTestFails.xml"; } else { $xmlFile = "xFail/expectedClmTestFails.xml"; } my $xml = XML::Lite->new($xmlFile); my $root = $xml->root_element(); if ($DEBUG) { print "_readXml $self->{_callingName}\n"; print "_readXml $self->{_compareGenerate}\n"; print "_readXml $xmlFile \n"; print ("_readXml Debug testType $testType \n"); print ("_readXml Debug machine $machine \n"); print ("_readXml Debug compiler $compiler \n"); } # Check for valid root node my $name = $root->get_name(); $name eq "expectedFails" or die "readExpectedFail.pm::_readXml :: $xmlFile is not a file that contains expected test failures\n"; my @e = $xml->elements_by_name($testType); $self->{_foundList}="FALSE"; ### populate list of tests for a specfic test type, machine and compiler ### there's got to be a better way to write this while ( my $e = shift @e ) { my @mChildren = $e->get_children(); foreach my $mChild (@mChildren) { my $mName=$mChild->get_name(); if ($mName eq $machine){ my @cChildren = $mChild->get_children(); foreach my $cChild (@cChildren) { my $cName=$cChild->get_name(); if ($cName eq $compiler) { my @cgChildren=$cChild->get_children(); foreach my $cgChild (@cgChildren) { my $cgName=$cgChild->get_name(); if($cgName eq $self->{_compareGenerate}){ @testList=$cgChild->get_children(); $self->{_foundList}="TRUE"; last; } } } } } } } if ($DEBUG) { print ("here 1 $self->{_foundList} \n"); } } ############################################################################## # ############################################################################## =head1 _testNowPassing reads the xml file for a particular machine, compiler, test type and (compare | generate) setup and saves it in memory for searching by _searchExpectedFail. _testNowPassing takes three arguments: $id - test id to print out $retVal - TRUE or FALSE. Was the id found in the expected fail list $xmlText - text from the XML notes section of the file. (Currently not used, may be used in future). returns: a text string =cut ############################################################################## # ############################################################################## sub _testNowPassing { my ($self, $id, $retVal, $xmlText) = @_ ; my $text=undef; if ($retVal eq "TRUE") { #_# found a test that passes now, but is listed as an xFail $text = "\n"; } else { #_# this test passes and was not previously listed as an xFail #_# noOp } return $text; } ############################################################################## # ############################################################################## =head1 _printOutput method that prints output for status files. _printOutput takes four arguments: $report - PASS,FAIL,xFAIL $testId - test id to print out $testReason - for xFAIL and new PASSES, additional reporting @testName - test description from original test returns: a text string =cut ############################################################################## # ############################################################################## sub _printOutput { my ($self, $report, $testId, $testReason, @testName) = @_ ; #_# print out the test results my $num=sprintf("%03d", $self->{_numericalTestId}); my $totNum=sprintf("%03d", $self->{_totTests}); print ("$num/$totNum <$report> $testReason \n"); } ############################################################################## # ############################################################################## =head1 _getTestType method that takes the name of the calling script and returns the type of test. Used for searching the expected fail list. _getTestType takes four arguments: $name - name of calling script returns: $type, the type of test =cut ############################################################################## # ############################################################################## sub _getTestType { my ($self, $name) = @_ ; if($DEBUG){ print ("_getTestType $name"); } my %testTypes = ( "build-namelist_test.pl" => "namelistTest", "test_driver.sh-i" => "clmInteractive", "test_driver.sh" => "clmBatch", "clm-cesm.sh" => "cesm" ); my $type = $testTypes {lc $name} || "unknown"; return $type; } ############################################################################## # ############################################################################## =head1 _getMachInfo method that figures out on what platform this is running and returns a 2 digit machine identifier and the compiler. This will eventually contain multiple compiler for various machines. _getMachInfo takes no arguments returns: $mach - the machine I'm running on $comp - the compiler being used =cut ############################################################################## # ############################################################################## sub _getMachInfo { my $name=`uname -n`; $name = substr($name, 0, 2); my %machNames = ( "ys" => "yellowstone", "fr" => "frankfurt" ); my %compNames = ( "ys" => "INTEL", "fr" => "INTEL" ); my $mach = $machNames {lc $name} || "unknown"; my $comp = $compNames {lc $name} || "unknown"; return ($mach,$comp); } # A Perl module must end with a true value or else it is considered not to # have loaded. By convention this value is usually 1 though it can be # any true value. A module can end with false to indicate failure but # this is rarely used and it would instead die() (exit with an error). 1;