Add the ability for tests to have a lines_diff attribute set for standard output datasets that will allow the specified number of lines to vary between test-data and tool run results; a 'change' to a line equates to a difference of 2 lines between files (i.e. 1 line added, 1 line removed). Add the ability for test comparisons to use single or multiline regular expressions, this is accessed by adding the attribute compare= "match_re" or "match_re_multiline" (respectively); the standard diff method can be explicitly declared as "diff", or is used at the default when not specified.

This commit is contained in:
Daniel Blankenberg
2010-03-05 17:21:16 -05:00
parent 9a6fb9b0a2
commit e31e8b8fd9
4 changed files with 106 additions and 43 deletions
+5 -2
View File
@@ -532,8 +532,11 @@ class Tool:
file = attrib.pop( 'file', None )
if file is None:
raise Exception( "Test output does not have a 'file'")
sort = util.string_as_bool( attrib.pop( 'sort', False ) )
test.add_output( name, file, sort )
attributes = Bunch()
attributes.compare = attrib.pop( 'compare', 'diff' ).lower() #method of comparison
attributes.lines_diff = int( attrib.pop( 'lines_diff', '0' ) ) # allow a few lines (dates etc) to vary in logs
attributes.sort = util.string_as_bool( attrib.pop( 'sort', False ) )
test.add_output( name, file, attributes )
except Exception, e:
test.error = True
test.exception = e
+2 -2
View File
@@ -37,8 +37,8 @@ class ToolTestBuilder( object ):
except Exception, e:
log.debug( "Error in add_param for %s: %s" % ( name, e ) )
self.inputs.append( ( name, value, extra ) )
def add_output( self, name, file, sort ):
self.outputs.append( ( name, file, sort ) )
def add_output( self, name, file, extra ):
self.outputs.append( ( name, file, extra ) )
def __expand_grouping_for_data_input( self, name, value, extra, grouping_name, grouping_value ):
# Currently handles grouping.Conditional and grouping.Repeat
if isinstance( grouping_value, grouping.Conditional ):
+97 -37
View File
@@ -36,14 +36,21 @@ class TwillTestCase( unittest.TestCase ):
#self.set_history()
# Functions associated with files
def files_diff( self, file1, file2, sort=False ):
def files_diff( self, file1, file2, attributes=None ):
"""Checks the contents of 2 files for differences"""
def get_lines_diff( diff ):
count = 0
for line in diff:
if ( line.startswith( '+' ) and not line.startswith( '+++' ) ) or ( line.startswith( '-' ) and not line.startswith( '---' ) ):
count += 1
return count
if not filecmp.cmp( file1, file2 ):
files_differ = False
local_file = open( file1, 'U' ).readlines()
history_data = open( file2, 'U' ).readlines()
if sort:
if attributes and attributes.sort:
history_data.sort()
##Why even bother with the check loop below, why not just use the diff output? This seems wasteful.
if len( local_file ) == len( history_data ):
for i in range( len( history_data ) ):
if local_file[i].rstrip( '\r\n' ) != history_data[i].rstrip( '\r\n' ):
@@ -52,37 +59,79 @@ class TwillTestCase( unittest.TestCase ):
else:
files_differ = True
if files_differ:
diff = difflib.unified_diff( local_file, history_data, "local_file", "history_data" )
diff_slice = list( islice( diff, 40 ) )
if file1.endswith( '.pdf' ) or file2.endswith( '.pdf' ):
# PDF files contain creation dates, modification dates, ids and descriptions that change with each
# new file, so we need to handle these differences. As long as the rest of the PDF file does
# not differ we're ok.
valid_diff_strs = [ 'description', 'createdate', 'creationdate', 'moddate', 'id' ]
valid_diff = False
for line in diff_slice:
# Make sure to lower case strings before checking.
line = line.lower()
# Diff lines will always start with a + or - character, but handle special cases: '--- local_file \n', '+++ history_data \n'
if ( line.startswith( '+' ) or line.startswith( '-' ) ) and line.find( 'local_file' ) < 0 and line.find( 'history_data' ) < 0:
for vdf in valid_diff_strs:
if line.find( vdf ) < 0:
valid_diff = False
else:
valid_diff = True
# Stop checking as soon as we know we have a valid difference
break
if not valid_diff:
# Print out diff_slice so we can see what failed
print "###### diff_slice ######"
raise AssertionError( "".join( diff_slice ) )
break
if attributes and attributes.lines_diff:
allowed_diff_count = attributes.lines_diff
else:
for line in diff_slice:
for char in line:
if ord( char ) > 128:
raise AssertionError( "Binary data detected, not displaying diff" )
raise AssertionError( "".join( diff_slice ) )
allowed_diff_count = 0
diff = list( difflib.unified_diff( local_file, history_data, "local_file", "history_data" ) )
diff_lines = get_lines_diff( diff )
if diff_lines > allowed_diff_count:
diff_slice = diff[0:40]
#FIXME: This pdf stuff is rather special cased and has not been updated to consider lines_diff
#due to unknown desired behavior when used in conjunction with a non-zero lines_diff
#PDF forgiveness can probably be handled better by not special casing by __extension__ here
#and instead using lines_diff or a regular expression matching
#or by creating and using a specialized pdf comparison function
if file1.endswith( '.pdf' ) or file2.endswith( '.pdf' ):
# PDF files contain creation dates, modification dates, ids and descriptions that change with each
# new file, so we need to handle these differences. As long as the rest of the PDF file does
# not differ we're ok.
valid_diff_strs = [ 'description', 'createdate', 'creationdate', 'moddate', 'id' ]
valid_diff = False
for line in diff_slice:
# Make sure to lower case strings before checking.
line = line.lower()
# Diff lines will always start with a + or - character, but handle special cases: '--- local_file \n', '+++ history_data \n'
if ( line.startswith( '+' ) or line.startswith( '-' ) ) and line.find( 'local_file' ) < 0 and line.find( 'history_data' ) < 0:
for vdf in valid_diff_strs:
if line.find( vdf ) < 0:
valid_diff = False
else:
valid_diff = True
# Stop checking as soon as we know we have a valid difference
break
if not valid_diff:
# Print out diff_slice so we can see what failed
print "###### diff_slice ######"
raise AssertionError( "".join( diff_slice ) )
else:
for line in diff_slice:
for char in line:
if ord( char ) > 128:
raise AssertionError( "Binary data detected, not displaying diff" )
raise AssertionError( "".join( diff_slice ) )
def files_re_match( self, file1, file2, attributes=None ):
"""Checks the contents of 2 files for differences using re.match"""
local_file = open( file1, 'U' ).readlines() #regex file
history_data = open( file2, 'U' ).readlines()
assert len( local_file ) == len( history_data ), 'Data File and Regular Expression File contain a different number of lines (%s != %s)' % ( len( local_file ), len( history_data ) )
if attributes and attributes.sort:
history_data.sort()
if attributes and attributes.lines_diff:
lines_diff = attributes.lines_diff
else:
lines_diff = 0
line_diff_count = 0
diffs = []
for i in range( len( history_data ) ):
if not re.match( local_file[i].rstrip( '\r\n' ), history_data[i].rstrip( '\r\n' ) ):
line_diff_count += 1
diffs.append( 'Regular Expression: %s\nData file : %s' % ( local_file[i].rstrip( '\r\n' ), history_data[i].rstrip( '\r\n' ) ) )
if line_diff_count > lines_diff:
raise AssertionError, "Regular expression did not match data file (allowed variants=%i):\n%s" % ( lines_diff, "".join( diffs ) )
def files_re_match_multiline( self, file1, file2, attributes=None ):
"""Checks the contents of 2 files for differences using re.match in multiline mode"""
local_file = open( file1, 'U' ).read() #regex file
if attributes and attributes.sort:
history_data = open( file2, 'U' ).readlines()
history_data.sort()
history_data = ''.join( history_data )
else:
history_data = open( file2, 'U' ).read()
#lines_diff not applicable to multiline matching
assert re.match( local_file, history_data, re.MULTILINE ), "Multiline Regular expression did not match data file"
def get_filename( self, filename ):
full = os.path.join( self.file_dir, filename)
@@ -541,7 +590,7 @@ class TwillTestCase( unittest.TestCase ):
hid = elem.get('hid')
hids.append(hid)
return hids
def verify_dataset_correctness( self, filename, hid=None, wait=True, maxseconds=120, sort=False ):
def verify_dataset_correctness( self, filename, hid=None, wait=True, maxseconds=120, attributes=None ):
"""Verifies that the attributes and contents of a history item meet expectations"""
if wait:
self.wait( maxseconds=maxseconds ) #wait for job to finish
@@ -576,13 +625,24 @@ class TwillTestCase( unittest.TestCase ):
data = self.last_page()
file( temp_name, 'wb' ).write(data)
try:
self.files_diff( local_name, temp_name, sort=sort )
if attributes and 'compare' in attributes:
compare = attributes.compare
else:
compare = 'diff'
if compare == 'diff':
self.files_diff( local_name, temp_name, attributes=attributes )
elif compare == 're_match':
self.files_re_match( local_name, temp_name, attributes=attributes )
elif compare == 're_match_multiline':
self.files_re_match_multiline( local_name, temp_name, attributes=attributes )
else:
raise Exception, 'Unimplemented Compare type: %s' % compare
except AssertionError, err:
os.remove(temp_name)
errmsg = 'History item %s different than expected, difference:\n' % hid
errmsg = 'History item %s different than expected, difference (using %s):\n' % ( hid, compare )
errmsg += str( err )
raise AssertionError( errmsg )
os.remove(temp_name)
finally:
os.remove( temp_name )
def verify_composite_datatype_file_content( self, file_name, hda_id ):
local_name = self.get_filename( file_name )
temp_name = self.get_filename( 'temp_%s' % file_name )
+2 -2
View File
@@ -66,13 +66,13 @@ class ToolTestCase( TwillTestCase ):
self.assertTrue( data_list )
elem_index = 0 - len( testdef.outputs )
for output_tuple in testdef.outputs:
name, file, sort = output_tuple
name, outfile, attributes = output_tuple
# Get the correct hid
elem = data_list[ elem_index ]
self.assertTrue( elem is not None )
elem_hid = elem.get( 'hid' )
elem_index += 1
self.verify_dataset_correctness( file, hid=elem_hid, maxseconds=testdef.maxseconds, sort=sort )
self.verify_dataset_correctness( outfile, hid=elem_hid, maxseconds=testdef.maxseconds, attributes=attributes )
self.delete_history( id=self.security.encode_id( latest_history.id ) )
def __expand_grouping( self, tool_inputs, declared_inputs, prefix='' ):