Ticket #10589: trac_10589--fixdoctest_failures-am.patch

File trac_10589--fixdoctest_failures-am.patch, 7.2 KB (added by andrew.mathas, 7 years ago)

Makes fixdoctests use difflib to generate diffs

  • sage-fixdoctests

    # HG changeset patch
    # User Andrew Mathas <andrew dot mathas at sydney dot edu dot au>
    # Date 1361770080 -39600
    # Node ID db1b5abbe44736f71181c096c09b036c78d8813a
    # Parent  a27561cc96a4affc9a79819e4c443890caf470f7
    Fixes failures in fixdoctests caused by the absence of expected values in the source file
    
    diff --git a/sage-fixdoctests b/sage-fixdoctests
    a b  
    44Given the output of doctest and a file, adjust the doctests so they won't fail.
    55
    66Doctest failures due to exceptions are ignored.
     7
     8AUTHORS::
     9
     10- Nicolas M. Thiery <nthiery at users dot sf dot net>  Initial version (2008?)
     11
     12- Andrew Mathas <andrew dot mathas at sydney dot edu dot au> 2013-02-14
     13  Cleaned up the code and hacked it so that the script can now cope with the
     14  situations when either the expected output or computed output are empty.
     15  Added doctest to sage.tests.cmdline
     16
    717"""
    818
    9 import os, sys
     19import os, sys, difflib
     20from optparse import OptionParser
     21from sage.misc.temporary_file import tmp_dir
    1022
    11 if not (len(sys.argv) in [2,3]) :
    12     print "Usage: sage -fixdoctests <source file> [doctest output]"
    13     print "Creates file <source file>.out that would pass the doctests (modulo raised exception)"
     23usage=r'''usage: sage --fixdoctests <source file> [doctest output] [--long]
     24Creates a file <source file>.out that passes the doctests (modulo any raised exceptions)'''
     25parser = OptionParser(usage=usage)
     26parser.add_option('-l','--long',dest='long', action="store_true", default=False)
     27
     28options,args = parser.parse_args()
     29
     30if not (len(args) in [1,2]) :
     31    print usage
    1432    sys.exit(1)
    1533
    16 if len(sys.argv) == 2:
    17     os.system('sage -t %s > .tmp'%sys.argv[1])
    18     doc_out = '.tmp'
    19 else:
    20     doc_out = sys.argv[2]
     34# set input and output files
     35test_file=args[0]
     36test_output=args[1] if len(args)==2 else test_file+'.out'
     37try:
     38    with open(test_file,'r') as src:
     39        src_in = src.read()
     40        pass
     41except IOError:
     42    raise ValueError('The file "%s" either does not exist or it is not readable' % test_file )
     43    sys.exit(1)
    2144
    22 src_in = open(sys.argv[1]).read()
     45# put the output of the test into sage's temporary directory
     46doc_out=tmp_dir()+'.tmp'
     47os.system('sage -t %s %s > %s'%('--long' if options.long else '', test_file, doc_out))
     48
    2349doc_out = open(doc_out).read()
    24 
    2550sep = "**********************************************************************\n"
    2651
    2752doctests = doc_out.split(sep)
    28 src_in_lines = src_in.split('\n')
     53src_in_lines = src_in.splitlines()
    2954
    30 v = src_in.splitlines()
    31    
    32 for X in doctests:
    33     if 'raceback' in X: continue
     55for block in doctests:
     56    if not 'Failed example:' in block: continue  # sanity checking, but shouldn't happen
    3457
    3558    # Extract the line, what was expected, and was got.
    36     i0 = X.find('line ')
    37     i = X.find(':\n')
    38     if i == -1: continue
    39     j = X.find('Expected:\n')
    40     if j == -1:
     59    line = block.find('line ')             # block should contain:  'line ##, in ...', where ## is an integer
     60    comma = block.find(', in ')            # we try to extract the line number which gives the test failure
     61    if line == -1 or comma==-1: continue   # but if something goes wrong we give up
     62    line_num=eval(block[line+5:comma])
     63    if 'Expected nothing' in block:
     64        exp = block.find('Expected nothing')
     65        block='\n'+block[exp+17:]  # so that split('\nGot:\n') does not fail below
     66    elif 'Expected:' in block:
     67        exp = block.find('Expected:\n')
     68        block=block[exp+10:]
     69    else:
    4170        continue
    42     k = eval(X[i0+5:i])
    43     line = X[i+1:j].strip()
    44     X = X[j+10:]
    45     expected, got = X.split('\nGot:\n')
    46     expected = expected.splitlines()
    47     got = got.splitlines()
    48     # Guess compute by how much got should be further indented to match with the
    49     # indentation of v
    50     n = (len(v[k]) - len(v[k].lstrip())) - (len(got[0]) - len(got[0].lstrip()))
     71    if block[-21:]=='Got:\n    <BLANKLINE>\n':
     72        expected=block[:-22]
     73        got=['']
     74    else:
     75        expected, got = block.split('\nGot:\n')
     76        got = got.splitlines()      # got can't be the empty string
    5177
    52     # Double check that expected was indeed there in the file
    53     if n < 0 or any( expected[i].strip() != v[k+i].strip() for i in range(len(expected)) ):
    54         import warnings
    55         warnings.warn("Did not manage to replace %s with %s"%('\n'.join(expected), '\n'.join(got)))
     78
     79    # If we expected nothing, and got something, then we need to insert the line before line_num
     80    # and match indentation with line number line_num-1
     81    if expected=='':
     82        indent = (len(src_in_lines[line_num-1]) - len(src_in_lines[line_num-1].lstrip()))
     83        src_in_lines[line_num-1]+='\n'+'\n'.join('%s%s'%(' '*indent,line.lstrip()) for line in got)
    5684        continue
    5785
    58     # Stuff all of `got` in v[k] and mark the remaining `expected` lines as to be ignored
    59     # so as to preserve the line numbering
    60     v[k] = '\n'.join( (' '*n + got[i]) for i in range(len(got)) )
     86    # Guess how much extra indenting got needs to match with the indentation
     87    # of src_in_lines - we match the indentation with the line in ``got`` which
     88    # has the smallest indentation after lstrip(). Note that the amount of indentation
     89    # required could be negative if the ``got`` block is indented. In this case
     90    # ``indent`` is set to zero.
     91    indent = max(0,(len(src_in_lines[line_num]) - len(src_in_lines[line_num].lstrip())
     92              - min(len(got[j]) - len(got[j].lstrip()) for j in range(len(got)))))
     93
     94    expected=expected.splitlines()
     95
     96    # Double check that what was expected was indeed in the source file and if
     97    # it is not then then print a warning for the user which contains the
     98    # problematic lines.
     99    if any( expected[i].strip() != src_in_lines[line_num+i].strip() for i in range(len(expected)) ):
     100        import warnings
     101        warnings.warn("Did not manage to replace\n%s\n%s\n%s\nwith\n%s\n%s\n%s"%(
     102                         '>'*40,'\n'.join(expected),'>'*40,'<'*40, '\n'.join(got), '<'*40))
     103        continue
     104
     105    # If we got something when we expected nothing then we delete the line from the
     106    # output, otherwise, add all of what we `got` onto the end of src_in_lines[line_num]
     107    if got==['']:
     108        src_in_lines[line_num] = None
     109    else:
     110        src_in_lines[line_num] = '\n'.join( (' '*indent + got[i]) for i in range(len(got)) )
     111
     112    # Mark any remaining `expected` lines as ``None`` so as to preserve the line numbering
    61113    for i in range(1, len(expected)):
    62         v[k+i] = None
     114        src_in_lines[line_num+i] = None
    63115
    64 src_out = ''.join(line+'\n' for line in v if line is not None)
    65116
    66 open(sys.argv[1] + '.out','w').write(src_out)
     117# We're done, so (try and) create and write the output file
     118while True:
     119    try:
     120        open(test_output,'w').write('\n'.join( line for line in src_in_lines if line is not None ))
     121        break
     122    except IOError:
     123        test_output = raw_input('Cannot write to %s. Please give a new filename: ' % test_output)
    67124
    68 os.system('diff -u %s %s | more'%(sys.argv[1], sys.argv[1] + '.out'))
     125# ...and print a diff of the old and new files to stdout
     126with open(test_file,'r') as src:
     127    with open(test_output,'r') as output:
     128        sys.stdout.writelines(line for line in difflib.unified_diff(src.readlines(),output.readlines()))
    69129
    70130