#!/usr/bin/env python

#*****************************************************************************
#
#    Install script
#
#    Copyright (C) 2010 Simon A. King <simon.king@nuigalway.ie>
#
#  Distributed under the terms of the GNU General Public License (GPL),
#  version 2 or later (at your choice)
#
#    This code is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#    General Public License for more details.
#
#  The full text of the GPL is available at:
#
#                  http://www.gnu.org/licenses/
#*****************************************************************************

"""
NOTE:

For historical reasons, this script replicates much of Sage's current doctest
framework. At the time of writing it, the doctesting framework did not suffice
for our purpose. The tests are executed on a per-method basis, which makes it
easier to avoid side effects of caching.

TODO:

Try to use Sage's doctest framework in version 3.0 of this spkg.

USAGE:

In a Sage shell, run spkg-check. If SAGE_CHECK is "yes",
the script will be automatically executed when the package
is installed.

The script does parallel testing, the number of thready being given
by the environment variable SAGE_NUM_THREADS, which is automatically set when
exporting MAKE="make -jN".

Note that each test uses at most three processes, namely Sage, GAP and Singular.

"""

import sage
#import sage.all
from sage.all import walltime, parallel#, ceil
import multiprocessing
import os
import inspect 
import sys

from sage.all import tmp_dir
test_dir = tmp_dir()

class FILE_NR:
    lfd_nr = -1
    def __call__(self):
        self.lfd_nr += 1
        return self.lfd_nr

file_nr = FILE_NR()

def get_classes(f,root):
    """
    List all class definitions (including cdef) in a string.
    
    INPUT:

    - ``f`` : string, usually obtained by reading a Python or Cython file
    - ``root``: string, the name of the module defined by ``f``.

    OUTPUT:

    A list of pairs ``(n,s)``, where ``n`` is the line number in ``f`` in
    which the class definition starts (counted from zero), and ``s`` is the
    name of the class (including the given module name ``root``).

    """
    import os
    m = []
    L = f.split(os.linesep)
    for i in range(len(L)):
        s = L[i].strip()
        while '  ' in s:
            s = s.replace('  ',' ')
        if (s.startswith('class ') or s.startswith('cdef class ')) and s.endswith(':'):
            name = s.split('class')[1][:-1] # remove ':' at the end
            name = (name.split('(')[0]).strip()
            m.append((i,root+'.'+name))
    return m

            
def get_methods(f,cl,nextcl=None, funcs = False):
    """
    List all method definitions (including cdef and cpdef) of a class in a string.

    INPUT:

    - ``f`` : string, usually obtained by reading a Python or Cython file
    - ``cl``: pair ``(n,s)``, where ``n`` is the line number in ``f`` in
      which the class definition starts (counted from zero), and ``s`` is the
      name of the class.
    - ``nextcl``: optional pair ``(n,s)``, where ``n`` is the line number in ``f`` in
      which the class definition *after* ``cl`` starts (counted from zero), and ``s``
      is the name of the class.

    OUTPUT:

    A list of pairs ``(n,s)``, where ``n`` is the line number in ``f`` in
    which the method definition starts (counted from zero), and ``s`` is the
    name of the method (including the name of the given class).

    """
    import os
    m = []
    ind = []
    j,root = cl
    if nextcl is not None:
        L = f.split(os.linesep)[j:nextcl[0]] #file(f).read().split('\n')[j+1:nextcl[0]]
    else:
        L = f.split(os.linesep)[j:] #file(f).read().split('\n')[j+1:]
    if not funcs:
        basic_ind = len(L[0])-len(L[0].lstrip())
        # first, get the right indentation level for methods of cl
        for i in range(1,len(L)):
            s = L[i].strip()
            if (s.startswith('def') or s.startswith('cdef')) and s.endswith(':'):
                if len(L[i])-len(L[i].lstrip()) <= basic_ind:
                    L = L[:i]
                    break
                ind.append(len(L[i])-len(L[i].lstrip()))
        if not ind:
            return []
        ind = min(ind)
    else:
        ind = -1
    for i in range(len(L)):
        s = L[i]
        if (s.startswith((ind*' ')+'def ') or s.startswith((ind*' ')+'cdef ') or s.startswith((ind*' ')+'cpdef ')) and s.endswith(':') and not s.startswith((ind*' ')+'cdef class '):
            # we found a method
            s = s.strip()[:-1]
            while '  ' in s:
                s = s.replace('  ',' ')
            if s.startswith('def'):
                name = (s.split(' ')[1]).split('(')[0].strip()
            else:
                name = (s.split(' ')[2]).split('(')[0].strip()
            m.append((j+i+1,root+'.'+name,s.split(' ')[0]))
    return m
    
def get_docstring(f,cl):
    """
    Return the docstring of a class, method or module

    INPUT:

    - ``f`` : string, usually obtained by reading a Python or Cython file
    - ``cl``: pair ``(n,s)``, where ``n`` is the line number in ``f`` in
      which the definition of the class, method or module starts (counted from zero),
      and ``s`` is the name of the class, method or module.

    NOTE:

    The docstring can be a single line, it can be included in single or double
    quotes.

    OUTPUT:

    The docstring of the given class, method or module.

    """
    import os
    j,root = cl[:2]
    L = f.split(os.linesep)[j:]
    OUT = []
    if not L:
        return ''
    while(1):
        p = L.pop(0).strip()
        if p.startswith('cdef class'):
            continue
        if p.startswith('class'):
            continue
        if p and (not p.startswith('#')):
            break
        if not L:
            return ''
    if p.startswith('"') or p.startswith('r"'):
        doub = True
        if not (p.startswith('"""') or p.startswith('r"""')): # single-lined docstring
            return p.split('"')[1]
    elif p.startswith("'") or p.startswith("r'"):
        doub = False
        if not (p.startswith("'''") or p.startswith("r'''")): # single-lined docstring
            return p.split("'")[1]
    else: # no docstring
        return ''
    while (L):
        p = L.pop(0)
        ps = p.strip()
        if (doub and ps.endswith('"""')):
            return os.linesep.join(OUT)
        elif ((not doub) and ps.endswith("'''")):
            return os.linesep.join(OUT)
        OUT.append(p)
    return ''

import os, sys
from sage.all import gap,singular
NCPUS = int(os.environ.get('SAGE_NUM_THREADS'))
if NCPUS>1 and gap._local_tmpfile()==singular._local_tmpfile():
    print "WARNING: You should apply the patch of ticket #10004."
    print "         Without the patch, parallel testing isn't safe."
    NCPUS = 1
    
@parallel(ncpus=NCPUS)
def one_test(s,cl,lfd_nr):
    """
    Perform the tests provided by a given docstring.

    INPUT:

    - ``s``: a string, which is usually a docstring of a class, method or module.
    - ``cl``: a string, the name of the class, method or module being tested.
    
    OUTPUT:

    A pair ``(msg1, msg2, t)``, where ``msg1`` is empty if the test succeeds and
    is an error report otherwise, and ``msg2`` is empty if the test passes certain
    formal criteria and is a problem report otherwise.
    ``t`` is the wall time in seconds required for that test.

    """
    import os
    if not s:
        print cl+' has no docstring'
        return ('',cl+': No docstring',0.0)
    if "not tested" in s:
        print cl+' is not tested'
        return ('',cl+': Not tested',0.0)
    if ('sage: ' not in s) or (('TEST' not in s) and ('EXAMPLE' not in s)):
        print cl+' has no doctest'
        return ('',cl+': No doctest',0.0)
    if 'indirect doctest' in s:
        prob = ''
    else:
        prob = cl+' seems not to be tested in its docstring'
        for l in s.split(os.linesep):
            l = l.strip()
            if l.startswith('sage: ') or l.startswith('... '):
                if cl.split('.')[-1] in l:
                    prob = ''
                    break
    from sage.all import walltime
    f = file(os.path.join(test_dir,'file_%d.py'%lfd_nr),'w')
    f.write('# -*- coding: utf-8 -*-'+os.linesep+'r"""'+os.linesep+s+os.linesep+'"""'+os.linesep)
    os.fsync(f)
    f.close()
    wt = walltime()
    Res = os.popen('sage -t --long '+f.name).read()
    wt = walltime(wt)
    os.unlink(f.name)
    if 'All tests passed!' not in Res.split(os.linesep)[-6:]:
        print cl + " fails"
        return (Res,prob,wt)
    print cl + " passes in %.2f s"%wt
    sys.stdout.flush()
    return ('',prob,wt)

def test_module(m, name):
    """
    Perform all doctests of a module

    INPUT:

    ``m``: a string, providing the file name (including path) of the
           file defining the module being tested.
    ``name``: a string, the name of the module

    OUTPUT:

    Two lists ``Error`` and ``Problem``. The first is a list of pairs
    ``(s,msg)``, where ``s`` is the name of a class, method or module
    whose doctest fails with message ``msg``. The second is a list
    of pairs ``(s,msg)``, where ``s`` is the name of a class, method
    or module whose doctest is problematic for the reason explained
    in ``msg``.

    NOTE:

    It is allowed that the module itself has no doctest. But all
    functions and classes in the module and all methods of all
    classes must have a test.

    """
    f = file(m).read()
    L = [(get_docstring(f, (0,name)), name, 0)]
    lfd_nr = 0
    for M in get_methods(f,(0,name),funcs=True): # these are functions
        lfd_nr += 1
        L.append((get_docstring(f,M), M[1],lfd_nr))
    Cl = get_classes(f,name)
    for i in range(len(Cl)):
        C = Cl[i]
        if i< len(Cl)-1:
            Cnext = Cl[i+1]
        else:
            Cnext = None
        lfd_nr += 1
        L.append((get_docstring(f,C), C[1], lfd_nr))
        for M in get_methods(f,C,Cnext):
            lfd_nr += 1
            L.append((get_docstring(f,M), M[1], lfd_nr))
    # now, L should provide the list of all docstrings
    ERR = []
    PROB = []

    
    for IN,res in one_test(L): # this does parallel testing!
        # IN is ((s,cl){}), where s is the docstring and cl the name
        if res[0]:
            ERR.append((IN[0][1],res[0]))
        if res[1] and (IN[0][1]!=name):
            PROB.append((IN[0][1],res[1]))
    return (ERR,PROB)


def test_all(d):
    """
    INPUT:

    ``d``: string, providing the path to the directory being tested

    OUTPUT:

    Test all '.py' and '.pyx' files in the given directory whose name
    does not start with a dot.
    """
    import os
    print 'Will use',NCPUS, 'parallel processes'+os.linesep
    D = os.listdir(d)
    base_name = os.path.split(d)[0]
    base_name = os.path.split(base_name)[1]
    to_do = []
    for n in D:
        if n=='__init__.py' or n=='all.py':
            to_do.append((d+n, base_name))
        elif (n.endswith('.py') or n.endswith('.pyx')) and not n.startswith('.'):
            to_do.append((d+n, base_name+'.'+n.split('.')[0]))
    ERR = []
    PROB = []
    from sage.all import walltime
    wt = walltime()
    for m,name in to_do:
        newERR, newPROB = test_module(m,name)
        ERR.extend(newERR)
        PROB.extend(newPROB)
    if ERR:
        print "There were doctest failures:"
        for a,b in sorted(ERR):
            print "%s:"%a
            print b
            print
        print "Summary"
        print "-------"
        print
        print "The following tests fail:"
        for a,b in sorted(ERR):
            print '  '+a
    else:
        print "-------------------------------------------------------------"
        print "All doc tests passed!"
        print "-------------------------------------------------------------"+os.linesep
    if PROB:
        print "The following items may be insufficiently tested:"
        for a,b in sorted(PROB):
            print '  '+b
    print "Total time: %.2f sec"%walltime(wt)

if __name__ == '__main__':
    print "Testing the package pGroupCohomology..."
    test_all('src/pGroupCohomology/')
