#!/usr/bin/env python

####################################################################
# Doctest a single Python, Cython, Sage, TeX or ReST file, using
# Sage's custom doctesting tags and framework built on top of
# Python's doctest functionality.  In particular, this allows for
# sage: prompts, preparsing, optional doctests, random behavior, etc.
#
# This also searches for TAB characters.  If any are found in .py,
# .pyx, .spyx, or .sage files, then doctesting fails unless the string
# "SAGE_DOCTEST_ALLOW_TABS" is also present somewhere (anywhere!) in
# the file.
#
# Return value in process exit code:
# 0: all tests passed
# 1: file not found
# 2: KeyboardInterrupt
# 4: doctest process was terminated by a signal
# 8: the doctesting framework raised an exception
# 16: script called with bad options
# 32: (used internally in sage-ptest)
# 64: time out
# 128: failed doctests
####################################################################

# System imports
import os, re, sys, signal, time, shutil, tempfile, random

# if any of the following environment variables are set, use them for
# the timeout lengths.
TIMEOUT = os.getenv('SAGE_TIMEOUT')
TIMEOUT_LONG = os.getenv('SAGE_TIMEOUT_LONG')
TIMEOUT_VALGRIND = os.getenv('SAGE_TIMEOUT_VALGRIND')

if TIMEOUT is None:
    # the default timeout for doctests: 6 minutes (in seconds)    
    TIMEOUT = 6 * 60
else:
    # output from os.getenv is a string: convert to a number
    TIMEOUT = float(TIMEOUT)
    
if TIMEOUT_LONG is None:
    # the timeout value for long doctests: 30 minutes (in seconds)
    TIMEOUT_LONG = 30 * 60
else:
    TIMEOUT_LONG = float(TIMEOUT_LONG)
    
if TIMEOUT_VALGRIND is None:
    # the timeout for doctests running under valgrind tools: unreasonably long
    TIMEOUT_VALGRIND = 1024 * 1024
else:
    TIMEOUT_VALGRIND = float(TIMEOUT_VALGRIND)

long_time          = False
only_optional      = False
only_optional_tags = set([])
optional           = False
verbose            = False
random_order       = False         # default is that tests aren't run in random order

argv = sys.argv

import sage.misc.preparser

######################################################
# This code is copied from sage.misc.misc for speed:
######################################################
def is_64bit():
    return sys.maxint == 9223372036854775807

######################################################
# Temporary files
######################################################    
DOT_SAGE = os.environ['DOT_SAGE']
if 'SAGE_TESTDIR' not in os.environ or os.environ['SAGE_TESTDIR'] is "":
    SAGE_TESTDIR = os.path.join(DOT_SAGE, "tmp")
else:
    SAGE_TESTDIR = os.environ['SAGE_TESTDIR']

tmpfiles = [] # list of temporary files to be deleted if doctesting succeeds

def delete_tmpfiles():
    for f in tmpfiles:
        try:
            os.remove(f)
        except OSError:
            pass
    
######################################################
# Set environment variables
######################################################    
SAGE_ROOT = os.environ["SAGE_ROOT"]
if os.environ.has_key('SAGE_PATH'):
    os.environ["PYTHONPATH"] = os.environ["PYTHONPATH"] + ':' + os.environ['SAGE_PATH']


######################################################
# Custom flags for the valgrind modes
######################################################
logfile = ' --log-file=' + os.path.join(DOT_SAGE, 'valgrind', 'sage-%s') + ' '
try:
    SAGE_MEMCHECK_FLAGS = os.environ['SAGE_MEMCHECK_FLAGS']
    print SAGE_MEMCHECK_FLAGS
except:
    SAGE_MEMCHECK_FLAGS = " --leak-resolution=high %s --leak-check=full --num-callers=25 " % (logfile % 'memcheck.%p')

try:
    SAGE_MASSIF_FLAGS = os.environ['SAGE_MASSIF_FLAGS']
except:
    SAGE_MASSIF_FLAGS = " --depth=6 " + (logfile % 'massif.%p')

try:
    SAGE_CALLGRIND_FLAGS = os.environ['SAGE_CALLGRIND_FLAGS']
except:
    SAGE_CALLGRIND_FLAGS = logfile % 'callgrind.%p'

try:
    SAGE_CACHEGRIND_FLAGS = os.environ['SAGE_CACHEGRIND_FLAGS']
except:
    SAGE_CALLGRIND_FLAGS = logfile % 'cachegrind.%p'

######################################################
# The Python binary
######################################################
PYTHON = "python"


def pad_zeros(s, size):
    """
    INPUT:
       - `s` -- something coercible to string
       - `size` -- integer

    Return s as a string with 0's padded on the left so the string has
    length exactly size.
    """
    s = str(s)
    return "0"*(size-len(s)) + s

index_cache = set([])
def new_index(n):
    """
    Return n-th example number.  If random_order is set, this is a randomly
    chosen number that we haven't returned before.  If random_order is not
    set, this just returns n.

    INPUT:
       - `n` -- integer
    """
    if random_order:
        while True:
            a = random.randint(0,2**32)
            if a not in index_cache:
                index_cache.add(a)
                return a
    else:
        return n

def test_code(filename):
    # Process exit codes for the generated doctest runner script:
    # 0: everything passed
    # 1-253: number of failed doctests
    # 254: >= 254 doctests failed
    # 255: exception raised by doctesting framework
    dict = { 'DIR'             : repr(os.path.join(SAGE_ROOT, 'local', 'bin')),
             'FILENAME'        : repr(filename),
             'OUTPUT_FILENAME' : repr(filename + '.timeit.sobj'),
             'TIMEIT'          : do_timeit, # global
             'VERBOSE'         : verbose }
    return """
if __name__ ==  '__main__':
    verbose = %(VERBOSE)s
    do_timeit = %(TIMEIT)s
    output_filename = %(OUTPUT_FILENAME)s

    import sys
    sys.path = sys.path + [%(DIR)s]
    import sagedoctest

    # execfile(%(FILENAME)s)
    m = sys.modules[__name__]
    m.__file__ = %(FILENAME)s

    try:

        # configure special sage doc test runner
        runner = sagedoctest.SageDocTestRunner(checker=None, verbose=verbose, optionflags=0)
        runner._collect_timeit_stats = do_timeit
        runner._reset_random_seed = True

        runner = sagedoctest.testmod_returning_runner(m,
                       # filename=%(FILENAME)s,
                       verbose=verbose,
                       globs=globals(),
                       runner=runner)
        runner.save_timeit_stats_to_file_named(output_filename)
    except:
        quit_sage(verbose=False)
        import traceback
        traceback.print_exc(file=sys.stdout)
        sys.exit(255)
    quit_sage(verbose=False)
    if runner.failures > 254:
        sys.exit(254)
    sys.exit(runner.failures)
""" % dict

NONE=0; LONG_TIME=1; RANDOM=2; OPTIONAL=3; NOT_IMPLEMENTED=4; NOT_TESTED=5; TOLERANCE=6
tolerance_pattern = re.compile(r'\b((?:abs(?:olute)?)|(?:rel(?:ative)?))? *?tol(?:erance)?\b( +[0-9.e+-]+)?')

def comment_modifier(s):
    sind = s.find('#')
    if sind == -1 or s[sind:sind+3] == '###':
        return [], ''
    eind = s.find('###',sind+1)
    L = s[sind+1:eind].lower()
    v = []
    if 'optional' in L:
        v.append(OPTIONAL)
    if 'known bug' in L:
        # Doctests marked like this should be automatically converted
        # to "optional bug", so they will be run by
        #
        #    sage -t ... --only-optional=bug
        #
        # So replace 'known' with '' or 'optional', depending on
        # whether 'optional' is already there.
        if 'optional' in L:
            L = L.replace('known', '')
        else:
            v.append(OPTIONAL)
            L = L.replace('known', 'optional')
    if 'long time' in L:
        v.append(LONG_TIME)
    if 'not implemented' in L:
        v.append(NOT_IMPLEMENTED)
    if 'not tested' in L:
        v.append(NOT_TESTED)
    if 'random' in L:
        v.append(RANDOM)
    m = tolerance_pattern.search(L)
    if m:
        v.append(TOLERANCE)
        rel_or_abs, epsilon = m.groups()
        if rel_or_abs is not None:
            rel_or_abs = rel_or_abs[:3]
        if epsilon is None:
            epsilon = 1e-15
        else:
            epsilon = float(epsilon.strip())
        v.append((rel_or_abs, epsilon))
    return v, L

def close_tolerance(comment_tags):
    rel_or_abs, epsilon = comment_tags[comment_tags.index(TOLERANCE) + 1]
    return "... ''', res, %r%s)" % (epsilon, '' if rel_or_abs is None else ", '%s'" % rel_or_abs)

def preparse_line_with_prompt(L):
    i = L.find(':')
    if i == -1:
        return sage.misc.preparser.preparse(L)
    else:
        return L[:i+1] + sage.misc.preparser.preparse(L[i+1:])

def doc_preparse(s):
    """
    Run the preparser on the documentation string s.
    This *only* preparses the input lines, i.e., those
    that begin with "sage:".or with "..."
    """
    sl = s.lower()

    # t: Deal with code whose output should be ignored. 
    t = []

    # used for adapting the output based on comments
    has_tolerance = False
    comment_modifiers = []
    last_prompt_comment = ''
    
    for L in s.splitlines():
        if not L.strip():
            if has_tolerance:
                t.append(close_tolerance(c) + old_cmd)
                has_tolerance = False
        
        begin = L.lstrip()[:5]
        comment = ''
        if begin == 'sage:':
            if has_tolerance:
                # old_cmd contains the command being tested along with
                # the line number.
                t.append(close_tolerance(c) + old_cmd)
            c, comment = comment_modifier(L)
            last_prompt_comment = comment
            line = ''
            if LONG_TIME in c and not long_time:
                L = '\n'  # extra line so output ignored
            if RANDOM in c:
                L = L.replace('sage:', 'sage: print "ignore this"; ') 
            if NOT_TESTED in c:
                L = '\n'   # not tested
            if NOT_IMPLEMENTED in c:
                L = '\n'   # not tested
            if OPTIONAL in c and not (only_optional or optional):
                L = '\n'
            if TOLERANCE in c:
                t.append(">>> res = Exception")
                L = "sage: res = %s" % L.lstrip()[5:]
                has_tolerance = True
            else:
                has_tolerance = False
            line = preparse_line_with_prompt(L)
            if RANDOM in c:
                # count spaces at the beginning of line to fix alignment later
                i = 0
                while i < len(line) and line[i].isspace():
                    i += 1
                # append a line saying 'ignore' followed by ellipsis (...)
                # and an empty line, to ignore the output given in the test
                line += '\n' + ' '*i + 'ignore ...\n'
            if has_tolerance:
                # save the current command along with the line number
                try:
                    idx = line.index('###_sage"line')
                    old_cmd = line[idx:]
                except IndexError:
                    old_cmd = ''
                line += "\n>>> check_with_tolerance('''"
            t.append(line)
            
        elif begin.startswith('...'):
            comment = last_prompt_comment
            i = L.find('.')
            t.append(L[:i+3] + sage.misc.preparser.preparse(L[i+3:]))
            
        else:
            comment = last_prompt_comment
            if has_tolerance:
                L = "... " + L
            t.append(L)
        
        comment_modifiers.append(comment)
        if has_tolerance:
            # In this case, we added a line to the source code:
            #      ">>> res = Exception"
            # So we need to add 'comment' one more time to
            # comment_modifiers so that the length of the list of
            # lines agrees with the length of comment_modifiers.
            comment_modifiers.append(comment)

    if has_tolerance:
        t.append(close_tolerance(c))

    # The very last line -- which is typically """ -- must never be marked as optional,
    # or it might not get included, which would be a syntax error. 
    comment_modifiers[-1] = ''

    # if only_optional is True, then we skip the entire block unless certain
    # conditions are met:
    #  1. If the tag list is empty, some line must contain # optional
    #  2. If the tag list is nonempty, some line must contain
    #                 # optional - nonempty,subset,of,tags
    #     In this case, we also strip out all #optional lines that don't
    #     contain a nonempty subset of tags, but keep all other lines.
    if only_optional:
        # check if any doctest is optional
        contains_optional_doctests = (len([x for x in comment_modifiers if 'optional' in x]) > 0)
        if len(only_optional_tags) == 0:
            # case 1
            if not contains_optional_doctests:
                t = []
        else:
            # case 2
            # first test that some line contains nonempty subset of tags, since
            # if not we won't bother at all.
            if contains_optional_doctests:
                # make a list of each optional line cut out by our tag choices
                v = [i for i in range(len(t)) if only_optional_include(comment_modifiers[i])]
                if len(v) == 0:
                    # if v is empty, don't test this block at all.
                    t = []
                else:
                    # otherwise, we test everything that is non-optional along with everything
                    # listed in v.
                    v = set(v)
                    t = [t[i] for i in range(len(t)) if  i in v  or  'optional' not in comment_modifiers[i]]
            else:
                t = []
    # Now put docstring together and return it.
    
    return '\n'.join(t)

def only_optional_include(s):
    """
    Return True if s is of the form
          # optional list,of,tags
    and the list of tags is a nonempty subset the global list only_optional_tags.
    If only_optional_tags is empty, then it is considered to be the
    set of all tags.

    INPUT:
        s -- a string
        only_optional_tags -- a global variable
    OUTPUT:
        bool 
    """
    i = s.find('optional')
    if i == -1:
        return False
    if len(only_optional_tags) == 0:
        # This doctest has # optional in it, but there are no tags, which
        # mean test everything that is optional.
        return True
    # Delete all white space, colons, periods, commas, and hyphens:
    s = ','.join(s[i+len('optional')+1:].translate(None, '-:,.').split())
    v = set(s.split(','))
    # Delete 'needs' and 'requires'.
    v.discard('needs')
    v.discard('requires')
    return (len(v) > 0 and v.issubset(only_optional_tags))


def extract_doc(file_name, library_code=False):
    """
    INPUT:
        file_name -- string; name of the file to extract the
                     docstrings from.
        library_code -- bool (default: False); if True, this file is
                    Sage core library code, so we do not import it,
                    and do doctest in a temporary directory.
    """
    F = open(file_name).readlines()
    # Put line numbers on every input line
    v = []
    i = 1
    j = 0
    while j < len(F):
        L = F[j].rstrip()
        if L.lstrip()[:5] == 'sage:':
            while j < len(F) and L.endswith('\\') and not L.endswith('\\\\'):
                j += 1
                i += 1
                L = L[:-1] + F[j].lstrip().lstrip('...').rstrip()
            L += '###_sage"line %s:_sage_    %s_sage"'%(i, L.strip())
        j += 1
        i += 1
        v.append(L)

    # 32/64-bit.  If we're on a 32-bit computer, remove all lines that
    # contains "# 64-bit", and if we're on a 64-bit machine remove all
    # lines that contain "# 32-bit".  This makes it possible to have
    # different output in the doctests for different bit machines.
    
    if is_64bit():
        exclude_string = "# 32-bit"
    else:
        exclude_string = "# 64-bit"
        
    F = '\n'.join([L for L in v if not exclude_string in L])

    if is_64bit():
        F = F.replace('# 64-bit','')
    else:
        F = F.replace('# 32-bit','')
    
    F = F.replace('\'"""\'','')

    root_name, ext = os.path.splitext(file_name)
    if ext == ".tex":
        F = pythonify_tex(F)
    elif ext == ".rst":
        F = pythonify_rst(F)
  
    s = "# -*- coding: utf-8 -*-\n"
    s += """
# This file was generated by 'sage-doctest' from
# '%s'.

"""  % os.path.join(os.getcwd(), file_name)
    s += "from sage.all_cmdline import *; \n"
    s += "import sage.plot.plot; sage.plot.plot.DOCTEST_MODE=True\n"  # turn off image popup
    s += r"""
def warning_function(f):
    import warnings

    def doctest_showwarning(message, category, filename, lineno, file=f, line=None):
        try:
            file.write(warnings.formatwarning(message, category, 'doctest', lineno, line))
        except IOError:
            pass # the file (probably stdout) is invalid
    return doctest_showwarning

def change_warning_output(file):
    import warnings
    warnings.showwarning = warning_function(file)

import re
float_pattern = re.compile('[+-]?((\d*\.?\d+)|(\d+\.?))([eE][+-]?\d+)?')

def check_with_tolerance(expected, actual, epsilon, rel_or_abs=None):
    if actual is Exception:
        return # error computing actual
    else:
        actual = str(actual)
    expected = re.sub('\n +', '\n', expected)
    actual = re.sub('\n +', '\n', actual)
    assert float_pattern.sub('#', expected.strip()) == float_pattern.sub('#', actual.strip()), \
        "Expected '" + expected + "' got '" + actual + "'"
    for expected_value, actual_value in zip(float_pattern.finditer(expected), float_pattern.finditer(actual)):
        expected_value = float(expected_value.group())
        actual_value = float(actual_value.group())
        if rel_or_abs == 'abs' or expected_value == 0:
            assert abs(expected_value - actual_value) < epsilon, "Out of tolerance %s vs %s" % (expected_value, actual_value)
        else:
            assert abs((expected_value - actual_value) / expected_value) < epsilon, "Out of tolerance %s vs %s" % (expected_value, actual_value)
"""

    if not library_code:
        if ext in ['.pyx','.spyx']:
            s += "cython(open('%s').read())\n\n" % file_name

        elif ext in ['.py', '.sage']:

            # For non-libary files, we need two different Python
            # files: one which contains the original Python code,
            # which we import, and one which contains the doctesting
            # framework.  First for FILE.py or FILE.sage, we replace
            # FILE with FILE_PID.  Then for FILE.sage, the imported
            # file is the preparsed version, so call that
            # FILE_PID_preparsed.py.  For FILE.py, the imported
            # version is just a copy of the original file, called
            # FILE_PID_orig.py.  The file containing doctesting
            # framework will be FILE_PID.py; this gets run through the
            # actual doctesting procedure by the 'test_file' function
            # below.
 
            root_name = os.path.basename(root_name)
            target_name = "%s_%d" % (root_name, os.getpid()) # like 'root_name', but unique
            target_base = os.path.join(SAGE_TESTDIR, target_name) # like 'target_name' but with full path

            if ext == '.sage':
                # TODO: preparse "<file>.sage" with a Sage library call
                # instead and write a string into temp_name.

                # For now: "sage -preparse <file>.sage" doesn't have any
                # output options and always creates <file>.py in the same
                # directory, so we first copy the *source* into SAGE_TESTDIR:
                os.system("cp '%s' %s.sage" % (file_name, target_base))
                # Now create SAGE_TESTDIR/<target_name>.py:
                os.system("sage -preparse %s.sage" % target_base)
                os.system("mv '%s'.py %s_preparsed.py" % (target_base, target_base))
                tmpfiles.append(target_base + ".sage")
                s += "\nfrom %s_preparsed import *\n\n" % target_name
                tmpfiles.append(target_base + "_preparsed.py") # preparsed version of original
                tmpfiles.append(target_base + "_preparsed.pyc") # compiled version
            else:
                # TODO: instead of copying the file, add its source
                # directory to PYTHONPATH.  We would also have to
                # import from 'name' instead of 'target_name'.
                os.system("cp '%s' %s_orig.py" % (file_name, target_base))
                s += "\nfrom %s_orig import *\n\n" % target_name
                tmpfiles.append(target_base + "_orig.py") # copied original
                tmpfiles.append(target_base + "_orig.pyc") # compiled version

            tmpfiles.append(target_base + ".py") # file with doctesting framework
            tmpfiles.append(target_base + ".pyc") # compiled version

    # Prefix/suffix for all doctests replacing the starting/ending """
    doc_prefix = 'r""">>> set_random_seed(0L)\n\n>>> change_warning_output(sys.stdout)\n\n'
    doc_suffix = '\n>>> sig_on_count()\n0\n"""'

    n = 0
    while True:
        i = F.find('"""')
        if i == -1: break
        k = F[i+3:].find('"""')
        if k == -1: break
        j = i+3 + k
        try:
            doc = doc_preparse(F[i:j+3])
        except SyntaxError:
            doc = F[i:j+3]
        if len(doc):
            doc = doc_prefix + doc[3:-3] + doc_suffix
            if random_order:
                n_str = pad_zeros(new_index(n),10)
            else:
                n_str = str(n)
            s += "def example_%s():" % n_str
            n += 1
            s += "\t"+ doc + "\n\n"
        F = F[j+3:]

    if n == 0:
        return  ''
    s += test_code(os.path.abspath(file_name))

    # Allow for "sage:" instead of the traditional Python ">>>".
    s = s.replace("sage:",">>>").replace('_sage"','')
    
    return s

def pythonify_tex(F):
    """
    INPUT:
        F -- string; read in latex file
    OUTPUT:
        string -- python program that has functions with docstrings made from the
                  verbatim examples in the latex file.
    """
    # Close links:
    F = F.replace('\\end{verbatim}%link','')
    F = F.replace('%link\n\\begin{verbatim}','')
    
    # Get rid of skipped code
    s = ''
    while True:
        i = F.find('%skip')
        if i == -1:
            s += F
            break
        s += F[:i]
        F = F[i:]
        j = F.find('\\end{verbatim}')
        if j == -1:
            break
        F = F[j + len('\\end{verbatim}')+1:]
    F = s
    
    # Make the verbatim environ's get extracted via the usual parser above
    F = F.replace("\\begin{verbatim}",'"""')
    F = F.replace("\\end{verbatim}",'"""')
    return F

def pythonify_rst(F):
    """
    INPUT:
        F -- string; read in ReST file
    OUTPUT:
        string -- python program that has functions with docstrings made from the
                  verbatim examples in the ReST file.
    """
    import re

    link_all = re.search(r'^\s*\.\.\s+linkall\s*$', F, re.M)

    def get_next_verbatim_block(s, pos):
        while True:
            # regular expression search in string s[pos:] for:
            # whitespace followed by "::" at the start of a line
            srch = re.search('^(\s*).*::\s*$', s[pos:], re.M)

            #Return -1 if we don't find anything.
            if srch is None:
               return "", -1, False

            pos += srch.start()

            prev_line = F.rfind("\n", 0, pos)
            start_of_options = F.rfind("\n", 0, prev_line if prev_line != -1 else 0)
            options = F[start_of_options:pos]

            pos += (srch.end() - srch.start())

            #Don't return skipped blocks
            if '.. skip' in options:
                continue

            #Check to see if we need to link up with the previous
            #block
            link_previous = link_all or ('.. link' in options)

            #Find the first line that isn't indented as much as
            #whatever followed the double colon.  The whitespace before
            #the double colon is stored in srch.groups()[0]
            lw = len(srch.groups()[0])  # length of whitespace before ::
            #So match anything of the form
            # beg. of line + whitespace of length at most lw + nonwhitespace
            no_indent = re.search("^\s{,%s}\S"%lw, s[pos:], re.M)

            if no_indent is None:
                return s[pos:], None, link_previous

            block, pos = s[pos:pos+no_indent.start()], pos+no_indent.start()
            if re.compile('^\s*sage: ', re.M).search(block) is not None:
                return block, pos, link_previous

    s = ''
    pos = 0
    while pos is not None and pos != -1:
        block, pos, link_previous = get_next_verbatim_block(F, pos)

        #Break if we can't find any more verbatim blocks
        if pos == -1:
            break

        #Check to see if we have to link this block
        #up with the previous block
        if link_previous and s != "":
            #Get rid of the trailing quotes and newlines from the
            #previous block
            s = s[:-5]

            s += '%s\n"""\n\n'%block
        else:
            s += '\n"""\n%s\n"""\n\n'%block

    #print s
    #print "--"*100
    return s

    

def post_process(s, file, tmpname):
    s = s.replace('.doctest_','')
    if file[-3:] == 'pyx':
        s = s.replace('"%s", line'%file[:-1], '"%s", line'%file)
    i = s.find("Failed example:")
    cnt = 0
    while i != -1:
        k = s[:i].rfind('File')
        k += s[k:].find(',')
        j = s[i:].find('###line')
        s = s[:k] + ', ' + s[i+j+3:]
        i = s.find("Failed example:")
        if i != -1:
            t = s[:i]
        else:
            t = s
        if t.find('check_with_tolerance') != -1:
            j = s.find('Exception raised')
            ass = 'AssertionError: '
            k = s.find(ass)
            s = s[:j] + s[k+len(ass):]
        i = s.find("Failed example:")
        cnt += 1
        if cnt > 1000:
            break
    s = s.replace(':_sage_',':\n').replace('>>>','sage:')
    c = '###line [0-9]*\n'
    r = re.compile(c)
    s = r.sub('\n',s)
    if cnt > 0:
        s += "For whitespace errors, see the file %s"%tmpname
    return (s, cnt)


def test_file(file, library_code):
    if os.path.exists(file):
        s = extract_doc(file, library_code=library_code)
        if len(s) == 0:
            delete_tmpfiles()
            sys.exit(0)

        name = os.path.basename(file)
        name = name[:name.find(".")]
        f = os.path.join(SAGE_TESTDIR, "%s_%d.py" % (name, os.getpid()))

        open(f,"w").write(s)
        tmpfiles.append(f)

        cmd = "%s %s"%(PYTHON, f)
        if gdb:
            print "*"*80
            print "Type r at the (gdb) prompt to run the doctests."
            print "Type bt if there is a crash to see a traceback."
            print "*"*80
            cmd = "gdb --args " + cmd

        if memcheck:
            cmd = "valgrind --tool=memcheck " + SAGE_MEMCHECK_FLAGS + cmd
        if massif:
            cmd = "valgrind --tool=massif " + SAGE_MASSIF_FLAGS + cmd
        if cachegrind:
            cmd = "valgrind --tool=cachegrind " +  SAGE_CACHEGRIND_FLAGS + cmd

        VALGRIND = os.path.join(DOT_SAGE, 'valgrind')
        try:
            os.makedirs(VALGRIND)
        except OSError:
            if not os.path.isdir(VALGRIND): 
                raise

        tm = time.time()
        try:
            out = ''; err = ''
            if verbose or gdb or memcheck or massif or cachegrind:
                import subprocess
                proc = subprocess.Popen(cmd, shell=True)
                while time.time()-tm <= TIMEOUT and proc.poll() == None:
                    time.sleep(0.1)
                if time.time()-tm >=TIMEOUT:
                    os.kill(proc.pid, 9)
                    print "*** *** Error: TIMED OUT! PROCESS KILLED! *** ***"
                e = proc.poll()
            else:
                outf = tempfile.NamedTemporaryFile()
                import subprocess
                proc = subprocess.Popen(cmd, shell=True, \
                        stdout=outf.file.fileno(), stderr = outf.file.fileno())
                while time.time()-tm <= TIMEOUT and proc.poll() == None:
                    time.sleep(0.1)
                if time.time()-tm >=TIMEOUT:
                    os.kill(proc.pid, 9)
                    print "*** *** Error: TIMED OUT! PROCESS KILLED! *** ***"
                outf.file.seek(0)
                out = outf.read()
                e = proc.poll()
        except KeyboardInterrupt:
            # TODO: if tests were interrupted but there were no failures, delete tmpfiles.
            print "KeyboardInterrupt -- interrupted after %.1f seconds!" % (time.time()-tm)
            sys.exit(2)
        if 'raise KeyboardInterrupt' in err:
            # TODO: if tests were interrupted but there were no failures, delete tmpfiles.
            print "*"*80 + "Control-C pressed -- interrupting doctests." + "*"*80
            sys.exit(2)

        if time.time() - tm >= TIMEOUT:
            print err
            sys.exit(64)

        s, numfail = post_process(out, file, f)
        s += err

        # search for tabs.  if found, the doctest fails, unless the string
        # "SAGE_DOCTEST_ALLOW_TABS" is also present somewhere in the file.
        ext = os.path.splitext(file)[1]
        if ext in [".py", ".pyx", ".sage", ".spyx"]:
            ff = open(file)
            source = ff.read()
            ff.close()
            if (source.find("SAGE_DOCTEST_ALLOW_TABS") == -1
                and source.find("\t") != -1):
                numfail += 1
                s = "*"*70 + "\n" + "Error: TAB character found.\n" + s
        if e == 255:
            # The doctesting code raised an exception
            print "Exception raised by doctesting framework. Use -verbose for details."
            sys.exit(8)

        if numfail == 0 and e > 0:
            numfail = e
        if numfail > 0:
            if not (verbose or gdb or memcheck or massif or cachegrind):
                print s
            sys.exit(128)
        elif e < 0:
            print "The doctested process was killed by signal %s" % (-e)
            sys.exit(4)
        else:
            delete_tmpfiles()
            sys.exit(0)
    else:
        print "Error running %s, since file %s does not exist."%(
            argv[0], argv[1])
        sys.exit(1)


def has_opt(opt):
    if '-' + opt in argv:
        i = argv.index('-' + opt)
        del argv[i]
        return True
    elif '--' + opt in argv:
        i = argv.index('--' + opt)
        del argv[i]
        return True
    return False

def parse_only_opt():
    """
    Search through argv for the -only-optional=list,of,tags option.
    If it is there, return True and a set of tags as a list of strings.
    If not, return False and an empty set.

    NOTE: Because it's very natural/easy to type only_optional instead
    of only-optional as an option (given that variable names have
    underscores in Python), using only_optional is accepted in
    addition to only-optional.

    OUTPUT:
        bool, set
    """
    for j, X in enumerate(argv):
        Z = X.lstrip('-')
        if Z.startswith('only-optional=') or Z.startswith('only_optional='):
            i = Z.find('=')
            del argv[j]
            return True, set(Z[i+1:].lower().split(','))
        elif Z.startswith('only-optional') or Z.startswith('only_optional'):  # no equals
            del argv[j]
            return True, set([])
    return False, []

def parse_rand():
    """
    Randomize seed and return True if we randomize doctest order.
    Otherwise we leave doctests in the "traditional order" and return False.
    """
    for j, X in enumerate(argv):
        Z = X.lstrip('-')
        if Z.startswith('randorder'):
            del argv[j]
            i = Z.find('=')
            if i != -1:
                random.seed(Z[i+1:])
            return True
    return False

def usage():
    print "\n\nUsage: sage-doctest [same options as sage -t] filenames"
    sys.exit(16)
        
if __name__ ==  '__main__':
    import os, sys
    if len(argv) == 1:
        usage()
    else:
        if has_opt('help') or has_opt('h') or has_opt('?'):
            usage()
        optional   = has_opt('optional')
        long_time  = has_opt('long')
        verbose    = has_opt('verbose')
        do_timeit  = has_opt('timeit')
        gdb        = has_opt('gdb')
        memcheck   = has_opt('memcheck') or has_opt('valgrind')
        massif     = has_opt('massif')
        cachegrind = has_opt('cachegrind')
        force_lib  = has_opt('force_lib')
        random_order = parse_rand()
        only_optional, only_optional_tags = parse_only_opt()
        if long_time:
            TIMEOUT = TIMEOUT_LONG
        if gdb or memcheck or massif or cachegrind:
            TIMEOUT = TIMEOUT_VALGRIND
        if argv[1][0] == '-':
            usage()
            
        ext = os.path.splitext(argv[1])[1]

        library_code = True
        dev_path = os.path.realpath(os.environ['SAGE_DEVEL'] + '/sage')
        our_path = os.path.realpath(argv[1])

        if not force_lib and (ext in ['.spyx', '.sage'] or
                              not dev_path in our_path):
            library_code = False

        try:
            test_file(argv[1], library_code = library_code)
        except KeyboardInterrupt:
            sys.exit(1)
