#!/usr/bin/env sage-python

"""
Given the output of doctest and a file, adjust the doctests so they won't fail.

Doctest failures due to exceptions are ignored.

AUTHORS::

- Nicolas M. Thiery <nthiery at users dot sf dot net>  Initial version (2008?)

- Andrew Mathas <andrew dot mathas at sydney dot edu dot au> 2013-02-14
  Cleaned up the code and hacked it so that the script can now cope with the
  situations when either the expected output or computed output are empty.
  Added doctest to sage.tests.cmdline

"""

import os, sys, difflib
import subprocess
from optparse import OptionParser
from sage.misc.temporary_file import tmp_dir

usage=r'''usage: sage --fixdoctests <source file> [doctest output] [--long]
Creates a file <source file>.out that passes the doctests (modulo any raised exceptions)'''
parser = OptionParser(usage=usage)
parser.add_option('-l','--long',dest='long', action="store_true", default=False)

options, args = parser.parse_args()

if not (len(args) in [1,2]) :
    print(usage)
    sys.exit(1)

# set input and output files
test_file = args[0]
test_output = args[1] if len(args)==2 else test_file
try:
    with open(test_file,'r') as src:
        src_in = src.read()
        pass
except IOError:
    raise ValueError('The file "%s" either does not exist or it is not readable' % test_file )
    sys.exit(1)

# put the output of the test into sage's temporary directory
doc_out = tmp_dir()+'.tmp'
os.system('sage -t %s %s > %s'%('--long' if options.long else '', test_file, doc_out))

doc_out = open(doc_out).read()
sep = "**********************************************************************\n"

doctests = doc_out.split(sep)
src_in_lines = src_in.splitlines()

for block in doctests:
    if not 'Failed example:' in block: continue  # sanity checking, but shouldn't happen

    # Extract the line, what was expected, and was got.
    line = block.find('line ')             # block should contain:  'line ##, in ...', where ## is an integer
    comma = block.find(', in ')            # we try to extract the line number which gives the test failure
    if line == -1 or comma==-1: continue   # but if something goes wrong we give up
    line_num=eval(block[line+5:comma])
    if 'Expected nothing' in block:
        exp = block.find('Expected nothing')
        block='\n'+block[exp+17:]  # so that split('\nGot:\n') does not fail below
    elif 'Expected:' in block:
        exp = block.find('Expected:\n')
        block=block[exp+10:]
    else:
        continue
    if block[-21:]=='Got:\n    <BLANKLINE>\n':
        expected=block[:-22]
        got=['']
    else:
        expected, got = block.split('\nGot:\n')
        got = got.splitlines()      # got can't be the empty string


    # If we expected nothing, and got something, then we need to insert the line before line_num
    # and match indentation with line number line_num-1
    if expected=='':
        indent = (len(src_in_lines[line_num-1]) - len(src_in_lines[line_num-1].lstrip()))
        src_in_lines[line_num-1]+='\n'+'\n'.join('%s%s'%(' '*indent,line.lstrip()) for line in got)
        continue

    # Guess how much extra indenting got needs to match with the indentation
    # of src_in_lines - we match the indentation with the line in ``got`` which
    # has the smallest indentation after lstrip(). Note that the amount of indentation
    # required could be negative if the ``got`` block is indented. In this case
    # ``indent`` is set to zero.
    indent = max(0,(len(src_in_lines[line_num]) - len(src_in_lines[line_num].lstrip())
              - min(len(got[j]) - len(got[j].lstrip()) for j in range(len(got)))))

    expected=expected.splitlines()

    # Double check that what was expected was indeed in the source file and if
    # it is not then then print a warning for the user which contains the
    # problematic lines.
    if any( expected[i].strip() != src_in_lines[line_num+i].strip() for i in range(len(expected)) ):
        import warnings
        warnings.warn("Did not manage to replace\n%s\n%s\n%s\nwith\n%s\n%s\n%s"%(
                         '>'*40,'\n'.join(expected),'>'*40,'<'*40, '\n'.join(got), '<'*40))
        continue

    # If we got something when we expected nothing then we delete the line from the
    # output, otherwise, add all of what we `got` onto the end of src_in_lines[line_num]
    if got==['']:
        src_in_lines[line_num] = None
    else:
        src_in_lines[line_num] = '\n'.join( (' '*indent + got[i]) for i in range(len(got)) )

    # Mark any remaining `expected` lines as ``None`` so as to preserve the line numbering
    for i in range(1, len(expected)):
        src_in_lines[line_num+i] = None


# Overwrite the source (or whatever output file was specified on the command line)
with open(test_output, 'w') as out:
    for line in src_in_lines:
        if line is None:
            continue
        out.write(line)
        out.write('\n')


# Show summary of changes
if test_file != test_output:
    print('The fixed doctests have been saved as {0}.'.format(test_output))
else:
    from sage.env import SAGE_ROOT
    relative = os.path.relpath(test_output, SAGE_ROOT)
    print(relative)
    if relative.startswith('..'):
        print('Fixed source file is not part of Sage.')
    else:
        subprocess.call(['git', 'diff', relative], cwd=SAGE_ROOT)

