#!/usr/bin/python
"""'epyunit' - Command line interface

The epyunit commandline interface provides a call wrapper
for unit and regression tests of arbitrary executables.
The wrapper internally relies on the standard packages 'PyUnit'
and integrates into Eclipse by 'PyDev'. Thus unit tests could
be applied in particular for shell scripts and intermixed 
application processes implemented in multiple programming 
languages. Automation of remote debugging by PyDev is 
supported.

The call is simply a prefix to the actual testee including 
it's options. The wrapper itself provides various criteria 
for the indication of the success and/or failure of the test 
case. Therefore correlation of stdout, stderr, and exit 
values is provided. 


SYNOPSIS:

  epyunit [OPTIONS] [--] <testee> [<testee-options>]

  rulesets:

    --exitign,  --exittype,  --exitval, --priotype
    --redebug, --redotall, --reignorecase, --remultiline, 
    --result, --resultnok, --resultok, --reunicode, 
    --stderrnok, --stderrok, --stdoutnok, --stdoutok, 


  output and format:
    
    --csv, --pass, --passall, --raw, --repr, --str, --xml
  
    --appname, --test-id, --timestamp

  process wrapper:
  
    --debug, --environment, --help, -Version, --Version,
    --verbose, -version, --version

    --selftest, --subproc, --subunit, 

    --exit-unit-ok, --exit-unit-failed

  subprocess debugging:

    --pydev-remote-debug, --rdbg, --rdbg-forward
  
  
OPTIONS:

  --appname=<arbitrary-name-of-app>

    An arbitrary application name to be inserted into record 
    headers.
    
  --csv

    Prints complete test result CSV format including header.

  -d --debug

     Debug entries, does NOT work with 'python -O ...'.
     Developer output, aimed for filtering.

  --environment

    Include platform info into header.

  --exitign=(True|False)

    Ignore exit value. 

  --exittype=(True|False)

    Exit value 'True' indicates success for '0',
    'False' indicates success for '!=0'.
    
  --exit-unit-failed=<exit-value>

    Exit value to be emitted in case of failure of unittest.
    
    default := 1
    
  --exit-unit-ok=<exit-value>

    Exit value to be emitted in case of success of unittest.
    
    default := 0

  --exitval=<exit-value>

    Indicates success when exit value is equal to the provided 
    value.

  -h --help

     This help.

  --pass

    Pass through the testee results on STDOUT and STDERR.
    The exit value is interpreted by rules, else the
    execution state of the framework defines the exit 
    value.
    
  --passall

    Pass through the testee result on STDOUT and STDERR
    including transparently the received exit value.

  --priotype

    In case of present failure and success conditions,
    
      TRUE:  the success condition dominates.
    
      FALSE: the failure condition dominates.

  --pydev-remote-debug[=host[:port]]

    Activates remote debugging with PyDev plugin of Eclipse.

  --raw 

    Same as '--passall'

  --rdbg[=host[:port]]

    Activate remode debug, optionally the host and port number
    of the server process could be changed. 

    default:=localhost:5678

  --rdbg-forward=(<forwarding-levels>|all|label)

    Forward the '--rdbg' option to subprocesses for nested debugging
    of process chains. 

        <forwarding-levels>: Number of levels to be forwarded, 0==None.
    
        all: all nested subprocesses
    
        label: An arbitrary label defined at initialization of the
        debug instance. Debugging is enabled when these match.
    
    default:=0: No forwarding.

  --redebug 

    Enables 're.DEBUG'

  --redotall: 

    Enables 're.DOTALL'
  
  --reignorecase: 

    Enables 're.IGNORECASE'.
  
  --remultiline: 

    Enables 're.MULTILINE'.
  
  --repr

    Prints complete test result by Python call of 'repr()'.

  --result=#total-results

    The treshold of the total matched results for changing
    the overall state to success. 

  --resultnok=#total-failure-results

    The treshold of the total matched failure results for
    changing the overall state to success. 

  --resultok=#total-success-results

    The treshold of the total matched success results for
    changing the overall state to success. 

  --reunicode: 

    Enables 're.UNICODE'.
  
  --selftest

     Performs a basic functional selftest by executing the 
     basic examples based on 'myscript.sh'.

  --stderrnok=<nok-string>

    Error string on stderr indicates success.

  --stderrok=<ok-string>

    OK string on stderr indicates success.

  --stdoutnok=<nok-string>

    Error string on stdout indicates success.

  --stdoutok=<ok-string>

    OK string on stdout indicates success.

  --str

    Prints complete test result by Python call of 'str()'.

  --subproc

    Starts the subprocess by: 'epyunit.SystemCalls'
    This mode also switches the output to '--passall' by 
    default, when another output mode is required, set 
    the required option after the '--subproc' option. 

  --subunit

    Starts the subprocess by the default: 'epyunit.SubprocessUnit'

  --test-id=<arbitrary-identifier-for-record-header>

    Prints the test-id with the formats 'csv', and 'xml'.
    Too be applied in case of multiple test case calls.

  --timestamp

    Includes date and time into record header.

  -Version --Version

     Current version - detailed.

  -v --verbose

     Verbose, some relevant states for basic analysis.
     When '--selftest' is set, repetition raises the display 
     level.

  -version --version

     Current version - terse.

  --xml

    Prints complete test result XML format.


ARGUMENTS:

  [--]

     The double hyphen terminates the options of the call, 
     thus the remaining part of the call is treated as the 
     subcall of the testee.

  <testee> 

     The wrapped testee.

  [<testee-options>]

     Options of the testee.


ENVIRONMENT:

  * PYTHON OPTIONS:

    -O, -OO: Eliminates '__debug__' code.
 

EXAMPLES:

  Basic call examples are provided:

  * `CLI: command line interface <epyunit_example_cli.html>`_ 

  * `Eclipse: PyDev integration <epyunit_example_eclipse.html>`_ 

  For detailed examples refer to the subdirectories of the 
  source package for:

  * Unit tests 

  * UseCases
  
COPYRIGHT:
  Arno-Can Uestuensoez @Ingenieurbuero Arno-Can Uestuensoez
  Copyright (C)2015-2016 Arno-Can Uestuensoez

"""
from __future__ import absolute_import
#from __future__ import print_function

__author__ = 'Arno-Can Uestuensoez'
__license__ = "Artistic-License-2.0 + Forced-Fairplay-Constraints"
__copyright__ = "Copyright (C) 2010-2016 Arno-Can Uestuensoez @Ingenieurbuero Arno-Can Uestuensoez"
__version__ = '0.1.13'
__uuid__='9de52399-7752-4633-9fdc-66c87a9200b8'

__docformat__ = "restructuredtext en"

#
#--- fetch options
#
import getopt, os, sys, platform

# name of application, used for several filenames as default
if '--appname' in sys.argv:
    _ai = sys.argv.index('--appname')
    _APPNAME = sys.argv[_ai]
else:
    _APPNAME = "epyunit"

# runtime environment 
_host = platform.node()
_user = "testuser"
_osu = platform.os.uname()
_os = _osu[0]
_osver = _osu[2]
_arch = _osu[-1]
_dist, _distver,_x = platform.dist()


#
# Remote debugging
#
from epyunit.checkRDbg import checkAndInitRDbg
_rdbgthis,_rdbg,_rdbgfwd = checkAndInitRDbg(**{'label':_APPNAME,})
"""
pydev remote debug options:
  _rdbgthis: requested debugging status for this process instance
  _rdbg: the remote debugging peer for this instance
  _rdbgfwd: requested state forwarding to nested subprocess levels
"""
if _rdbgthis:
    # activate remote debug stub call
    import epyunit.PyDevERDbg
    epyunit.PyDevERDbg.PYDEVD.startDebug() # start debugging here...
    #
    # remote breakpoints could be set from here on...
    #


# just to assure PYTHONPATH...
try:
    from epyunit.SystemCalls import SystemCalls
    from epyunit.SubprocUnit import SubprocessUnit
except Exception as e:
    print "\n#\n#*** Set 'PYTHONPATH' ("+str(e)+")\n#\n"
    sys.exit(1)

class  EPyUnitException(Exception):
    pass


def usage():
    if __name__ == '__main__':
        import pydoc
        #FIXME: literally displayed '__main__'
        print pydoc.help(__name__)
    else:
        help(str(os.path.basename(sys.argv[0]).split('.')[0]))

_longopts = [

    # result type and decision process
    "priotype=", "result=", "resultnok=", "resultok=",

    # exit values
    "exitval=","exitign=","exittype=",
    
    # output and error streams
    "stderrnok=","stdoutnok=","stderrok=","stdoutok=",
    "redebug","redotall","reignorecase","remultiline",
    "reunicode",
    
    
    # format
    "repr", "xml", "csv", "str", "pass", "passall", "raw",

    # runtime environent
    "appname=", "test-id=", "timestamp", "environment",
    "subproc", "subunit",

    "exit-unit-failed", "exit-unit-ok",

    # misc
    "help","debug","verbose","version","Version","selftest",

    #
    #-----
    #FIXME: 4DEL
    "default-nok", "default-ok",
]
_sopts = "a:hdv"

def usagemin():
    print "\nAvailable options:"
    slst = ""
    nl = 0
    print "\nshortopts:"
    for s in _sopts:
        if nl == 10:
            print "  "+slst
            nl = -1
            slst = ""
        if s == ':':
            continue
        slst += "-%s "%(s)
    if slst:
        print "  "+slst
        
    ilst = ""
    nl = 0
    print "\nlongopts:"
    for i in sorted(_longopts):
        if nl == 2  or nl>=len(_longopts):
            print "  "+ilst
            nl = -1
            ilst = ""
        ilst += "--%-20s "%(i)
        nl += 1
    if ilst:
        print "  "+ilst
    print """
Examples:

  epyunit -v --selftest
  epyunit -v myscript.sh NOK
  epyunit -v --exit=8 myscript.sh EXIT8


set PATH for 'myscript.sh' to 'epyunit' directory
use 'myscript.sh -h' for all response pattern
use '--help' for complete help

"""


_kargs={}
try:
    _opts, _args = getopt.getopt(sys.argv[1:], _sopts, _longopts)
except getopt.GetoptError, err:
    print str(err)
    usagemin()
    sys.exit(2)


#
# defaults
#

# name of tested application
_appname = None

# test id, to be printed with result data records
_testid = 0

# perform hard-coded basic selftest
_selftest = False

# verbose output
_verbose = 0

# debug output
_debug = 0

#
_default = 'OK'

# when OK and NOK conditions met the "NOK" defines the result
_prio = "NOK"

# exit value defined as success, or ignored: OK(0) | NOK(!=0) | IGNORE
_exit = "OK"
_exitokval = 0
_exitnok = "NOK"
_exitnokval = 1


# activate check of exit code: OK(0) else NOK(>0)
_chk_exit = True

# a strings to be checked in stderr stream
_chk_stderr = False
_CHK_STDERR_OK = [] # list of provided strings: OK condition
_CHK_STDERR_NOK = [] # list of provided strings: NOK condition

# a strings to be checked in stdout stream
_chk_stdout = False
_CHK_STDOUT_OK = [] # list of provided strings: OK condition
_CHK_STDOUT_NOK = [] # list of provided strings: NOK condition


# counter values for occured matches
_result = 0 # overall

# full result value display
_out = None
_timestamp = False
_environment = False

#
# for now one of each, last wins
_myRulesMap = {}

_O_REPR   = 0
_O_XML    = 1
_O_CSV    = 2
_O_PASS   = 3
_O_PASSA  = 4

#
# start wrapper for subprocess
_CALL_SUBPROC = SubprocessUnit 

for _o,_a in _opts:
    
    #-------------------------------------------------------------
    #
    # *** rules - stored in _myRulesMap ***
    #

    #
    # *** result types ***
    #
    if _o in ("--priotype",):
        if _a.lower() in ('true','1','ok',): 
            _myRulesMap['priotype'] = True
        else:
            _myRulesMap['priotype'] = False

    #
    # *** result thresholds ***
    #
    elif _o in ("--result",):
        if type(_a) is int: 
            _myRulesMap['result'] = _a
        else:
            raise EPyUnitException("Integer required:"+str(_a))
    elif _o in ("--resultnok",):
        if type(_a) is int: 
            _myRulesMap['resultnok'] = _a
        else:
            raise EPyUnitException("Integer required:"+str(_a))
    elif _o in ("--resultok",):
        if type(_a) is int: 
            _myRulesMap['resultok'] = _a
        else:
            raise EPyUnitException("Integer required:"+str(_a))

    #
    # *** exit ***
    #
    elif _o in ("--exitval",):
        _myRulesMap['exitval'] = int(_a)
    elif _o in ("--exitign",):
        if _a.lower() in ('true','1','ok',): 
            _myRulesMap['exitign'] = True
        else:
            _myRulesMap['exitign'] = False
    elif _o in ("--exittype",):
        if _a.lower() in ('true','1','ok',): 
            _myRulesMap['exittype'] = True
        else:
            _myRulesMap['exittype'] = False

    
    #
    # *** stderr ***
    #
    elif _o in ("--stderrnok",):
        _CHK_STDERR_NOK.append(_a)
    elif _o in ("--stderrok",):
        _CHK_STDERR_OK.append(_a)

    #
    # *** stdout ***
    #
    elif _o in ("--stdoutnok",):
        _CHK_STDOUT_NOK.append(_a)
    elif _o in ("--stdoutok",):
        _CHK_STDOUT_OK.append(_a)

#     elif _o in ("--default-ok",):
#         _myRulesMap['default'] = 'OK'
#     elif _o in ("--default-nok",):
#         _myRulesMap['default'] = 'NOK'

    #
    # flags for re
    # 
    elif _o in ("--redebug",):
        _myRulesMap['redebug'] = True 
    elif _o in ("--redotall",):
        _myRulesMap['dotall'] = True 
    elif _o in ("--reignorecase",):
        _myRulesMap['ignorecase'] = True 
    elif _o in ("--remultiline",):
        _myRulesMap['multiline'] = True 
    elif _o in ("--reunicode",):
        _myRulesMap['unicode'] = True 
        

    #-------------------------------------------------------------
    #
    # ** output format ***
    #
    elif _o in ("--str",):
        _kargs['out'] = 'str'
        _out = _O_REPR
    elif _o in ("--repr",):
        _kargs['out'] = 'repr'
        _out = _O_REPR
    elif _o in ("--xml",):
        _kargs['out'] = 'xml'
        _out = _O_XML
    elif _o in ("--csv",):
        _kargs['out'] = 'csv'
        _out = _O_CSV
    elif _o in ("--pass",):
        _kargs['out'] = 'pass'
        _out = _O_PASS
    elif _o in ("--passall",):
        _kargs['out'] = 'pass'
        _out = _O_PASSA
    elif _o in ("--raw",):
        _kargs['raw'] = True
        _kargs['out'] = 'pass'
        _out = _O_PASSA



    #-------------------------------------------------------------
    #
    # *** framework misc - passed by kargs ***
    #

    elif _o in ("-a","--appname",):
        _appname = _a
    elif _o in ("--test-id",):
        _testid = _a
    elif _o in ("--timestamp",):
        _timestamp = True
    elif _o in ("--environment",):
        _environment = True

    elif _o == "--selftest":
        _selftest = True

    elif _o == "--subproc":
        _CALL_SUBPROC = SystemCalls
        _kargs['out'] = 'pass'
        _out = _O_PASSA

    elif _o == "--subunit":
        _CALL_SUBPROC = SubprocessUnit

    elif _o == "--exit-unit-failed":
        _exitokval = _a

    elif _o == "--exit-unit-ok":
        _exitnokval = _a

    #
    # debug and trace
    #
    elif _o in ("-d","--debug",):
        _kargs['debug'] = True
        _debug += 1
    elif _o in ("-v","--verbose",):
        _verbose += 1

    #
    # help and info
    #
    elif _o in ("-h",):
        usagemin()
        sys.exit()

    elif _o in ("--help",):
        usage()
        sys.exit()

    elif _o in ("--version",):
        print str(__version__)
        sys.exit()

    elif _o in ("--Version",):
        print "app:      "+str(_APPNAME)
        print "version:  "+str(__version__)
        print "author:   "+str(__author__)
        print "copyright:"+str(__copyright__)
        print "license:  "+str(__license__)
        print "file:     "+str(os.path.basename(__file__))
        sys.exit()

    else:
        assert False, "unhandled option"+str(_o)


#
# assembled collections for rules
#
if _CHK_STDERR_NOK:
    _myRulesMap["stderrnok"] = _CHK_STDERR_NOK
if _CHK_STDERR_OK:
    _myRulesMap["stderrok"] = _CHK_STDERR_OK
if _CHK_STDOUT_NOK:
    _myRulesMap["stdoutnok"] = _CHK_STDOUT_NOK
if _CHK_STDOUT_OK:
    _myRulesMap["stdoutok"] = _CHK_STDOUT_OK


if _selftest: # do predefined selftest only
    import epyunit.selftest
    
    _myargs = {}
    if _out == _O_REPR:
        _myargs['out'] = 'repr'
    elif _out == _O_XML:
        _myargs['out'] = 'xml'
    elif _out == _O_PASS:
        _myargs['out'] = 'pass'
    elif _out == _O_PASSA:
        _myargs['out'] = 'pass-all'
    elif _out == _O_CSV:
        _myargs['out'] = 'csv'
    elif _verbose and not _out:
        _myargs['out'] = 'str'

    if _verbose:
        _myargs['verbose'] = _verbose
    if _debug:
        _myargs['debug'] = _debug

    if _rdbgfwd: # forward debugging status
        _myargs['rdbgforward'] = _rdbgfwd
        _myargs['rdbg'] = _rdbg
    
    epyunit.selftest.selftest(**_myargs)
    sys.exit(0)


#
# normal procedure...
#
if _verbose>0:
    _kargs['verbose'] = _verbose 
if _debug > 0:
    _kargs['debug'] = _debug 

if _out in (_O_PASS,_O_PASSA,):
    _kargs['raw'] = True

_kargs.update(_myRulesMap)
sx = _CALL_SUBPROC(**_kargs)
if _debug > 0:
    print str(sx)+"\n"
ret = sx.callit(' '.join(_args))
if ret[0] == 126:
    print >>sys.stderr ,"check exec permissions of 'myscript.sh'"
if _verbose+_debug > 0:
    print ret

# apply unittest filters
_unit_status = sx.apply(ret)

# display data
sx.displayit(ret)

# reply exit value
if _out in (_O_PASSA,): # pass all - including exit value of callee
    sys.exit(ret[0])

if _verbose or _debug:
    print "epyunit => "+str(_result)

if _unit_status:
    sys.exit(_exitokval)
else:
    sys.exit(_exitnokval)

#sys.exit(_result) # the exit value for the state of the wrapper itself
