#!/usr/bin/python
#
# tester.py: run Pathan 2 tests
#
# Copyright (c) 2003 DecisionSoft Ltd.
# Stephen White (swhite) Tue Feb 18 09:34:42 2003
#
# Id: $Id: tester,v 1.2 2004/07/30 09:18:09 jpcs Exp $

import getopt, sys, os, string, xml
from xml.dom.minidom import Document,parse,Node

try:
  import readline
except:
  pass # Windows or something inferior :)

useUnicode = 0
try:
  import codecs
  useUnicode = 1
except:
  pass # Python 1.5

on_cygwin = (string.find(os.uname()[0],"CYGWIN") == 0)

def asciiStr(str):
  if useUnicode:
    return str.encode('ascii','replace')
  else:
    return str

def utf8Str(str):
  if useUnicode:
    return str.encode('utf-8','strict')
  else:
    return str

#
# usage: display usage message
#
def usage():
  print """
usage: %s [ options ] directory

Runs a set of Pathan 2 tests from the given directory, according to the
tester.xml file contained in that directory.

Options:
   -h Displays this help message
   -x Displays help on the XML file format
   -f Run tests which are supposed to fail (instead)
   -a Run all tests
   -u Update the policy file if 'fail' tests are found to pass (only makes
      sense with either -a or -f)
   -v be more verbose
   -vv be very verbose
   -A/--add Add a 'pass' case, based on how the program currently behaves.
            This is interactive.

   -o convert old policy.test and base files into the new XML format.  This
      will almost certainly be broken in the not-too-distant future
      and should be removed.  You may wish to use xmlpp or an xml editor on
      the output!

""" % (sys.argv[0])
  sys.exit(0)

#
# xmlFormat: displays an annotated example of the XML file format
#
def xmlFormat():
  print """
<test purpose="some string">       <- define a test with a given purpose

  <policies>                       <- policies define how this test will be run

    <default>                      <- optional section that defines default
      <program>eval</program>         values that apply to all policies.
      <addArg>test.xml</addArg>
      <addOption name="-N">xs http://foo/</addOption>
    </default>

    <policy name='normal' />       <- this policy uses the default values

    <policy name='debug'>          <- this policy checks that the program
      <addOption name="-d" />         behaves in the same way when using the
    </policy>                         -d option

  </policies>

  <base type='pass'>               <- This is a test that should pass.
    <arg>string(.)</arg>
    <output policy='debug'>        <- Running the program according to the
      more text                       'debug' policy should produce this output
    </output>
    <output>some text</output>     <- Running the program according to every
  </base>                             other policy should produce this output
  
  <base type='fail' comment='not implemented yet'>
    <arg>unique-ID(/testcase/namespace::*[1])</arg>
    <output></output>
  </base>
</test>
"""
  sys.exit(0)

#
# Remove me, this converts the evil files used by the old perl codeinto fluffy
# XML
#
def convertOldFormat():
  testXML = Document()
  testroot = testXML.createElement("test")
  testXML.appendChild(testroot)
  
  policyf = open("policy.test","r")
  testroot.setAttribute("purpose",string.strip(policyf.readline()))
  
  policies = testXML.createElement("policies")
  testroot.appendChild(policies)
  
  defProg = None
  
  for l in policyf.readlines():
    if l[0]=='#':
      continue
    (name,cmd,args) = string.split(l+"\t\t","\t",2) # always exactly 3 elements
    command = string.split(string.strip(cmd))
    arguments = string.split(string.strip(args))
  
    if defProg == None:
      defProg = command[0]
  
    policy = testXML.createElement("policy")
    policy.setAttribute("name",name)
    policies.appendChild(policy)
  
    # Program name
    if command[0] != defProg:
      program = testXML.createElement("program")
      t = testXML.createTextNode(command[0])
      program.appendChild(t)
      policy.appendChild(program)
    del command[0]
    
    # Options
    while len(command) > 0:
      option = command[0]
      value = ""
      del command[0]
  
      while (len(command)>0) and (command[0][0]!="-"):
        value = string.strip(value + " " + command[0])
        del command[0]
  
      opt = testXML.createElement("addOption")
      opt.setAttribute("name",option)
      if value:
        t = testXML.createTextNode(value)
        opt.appendChild(t)
      policy.appendChild(opt)
  
    # Arguments
    for arg in arguments:
      if arg != "2>&1": # We do this regardless
        a = testXML.createElement("addArg")
        t = testXML.createTextNode(arg)
        a.appendChild(t)
        policy.appendChild(a)
  
  policyf.close()
  
  defaults = testXML.createElement("default")
  policies.appendChild(defaults)
  program = testXML.createElement("program")
  t = testXML.createTextNode(defProg)
  program.appendChild(t)
  defaults.appendChild(program)

  # In theory we should now scan all the policies and take any common options
  # and put them in the default section
  
  bases = open("base","r")
  while 1:
    s = string.strip(bases.readline())
    if not s:
      break
  
    while s[0]=='#':
      s = string.strip(bases.readline())
      
    (input,comment) = string.split(s+"##","#",1) # Ensure split is 2 parts
    
    while input[-1] == "\\": # Cope with escaped #s
      (i,comment) = string.split(comment,"#",1)
      input = input[:-1] + '#' + i
  
    comment = string.strip(string.replace(comment,"#"," "))

    input = string.replace(input,"PWD","$PWD$")
  
    type = "pass"
    
    if string.find(comment,"FAIL")==0:
      comment = string.strip(string.replace(comment,"FAIL",""))
      type = "fail"
  
    output = ''
    s = bases.readline()
    while s and (string.strip(s) != '%'):
      if s[0] != '#':
        output = output + string.replace(s,"\#","#") # Grr
      s = bases.readline()

    output = string.replace(output,"PWD","$PWD$")
  
    base = testXML.createElement("base")
    base.setAttribute("type",type)
    if comment:
      base.setAttribute("comment",comment)
      
    inp = testXML.createElement("arg")
    t = testXML.createTextNode(input)
    inp.appendChild(t)
    base.appendChild(inp)
    
    out = testXML.createElement("output")
    t = testXML.createTextNode(output)
    out.appendChild(t)
    base.appendChild(out)
    testroot.appendChild(base)
  
  bases.close()

  f = open("tester.xml","w")
  testXML.writexml(f)
  f.close()

#
# main: 
#
try:
  optlist, args = getopt.getopt(sys.argv[1:],"hfxovauA",["help","add","unordered"])
except getopt.error, e:
  print "Error:",e
  usage()

testType = 'pass'
convertOld = 0
verbose = 0
updateFile = 0
doAdd = 0
unorderedMode = 0

for o, a in optlist:
  if o in ["-h","--help"]: usage()
  if o in ["-A","--add"]: doAdd = 1
  if o=="-x": xmlFormat()
  if o=="-f": testType = 'fail'
  if o=="-a": testType = None
  if o=="-v": verbose = verbose + 1
  if o=="-o": convertOld = 1
  if o=="-u": updateFile = 1
  if o=="--unordered": unorderedMode = 1

if len(args)==0: usage()

os.chdir(args[0])

if convertOld:
  convertOldFormat()

#
# Get all direct (element) children of 'node' with the given name
#
def getChildrenCalled(name,node):
  list = []
  for n in node.childNodes:
    if (n.nodeType == Node.ELEMENT_NODE) and (n.localName == name):
      list.append(n)
  return list

#
# Gets a child of 'node' with the given name.  Raises an exception if
# there isn't exactly one child with the given name
#
def getChildCalled(name,node):
  l = getChildrenCalled(name,node)
  if len(l) == 1:
    return l[0]
  else:
    if len(l) == 0:
      raise "NoSuchChild",name
    else:
      raise "TooManyChildrenCalled",name

#
# Get the concatenation of all the TextNodes contained as direct children of
# the given node
#
def textValue(node):
  text = ""
  for n in node.childNodes:
    if n.nodeType == Node.TEXT_NODE:
      text = text + n.nodeValue
  return text

#
# Read tester.xml and run the tests!
#
try:
  testXML = parse('tester.xml')
except xml.sax._exceptions.SAXParseException, e:
  print "Error parsing XML in tester.xml:",os.getcwd()
  print e
  sys.exit(1)

testroot = testXML.documentElement

policies = getChildCalled("policies",testroot)

defaultArgs = []
defaultOpts = []
defaultProg = None

try:
  default = getChildCalled("default",policies)

  try:
    defaultProg = textValue(getChildCalled("program",default))
  except "NoSuchChild",name:
    pass

  for n in getChildrenCalled("addArg",default):
    defaultArgs.append(textValue(n))

  for n in getChildrenCalled("addOption",default):
    defaultOpts.append(n.getAttribute("name"),textValue(n))

except "NoSuchChild",name:
  pass # the default section is optional

totalFailures = 0
failPasses = []

def runTest(prog,opts,args,arg):
  arg = "'"+string.replace(arg,"'","'\"'\"'")+"'"
  arg = string.replace(arg,"$PWD$",os.getcwd())
  s = string.join([prog]+opts+[arg]+args+["2>&1"]," ")
  o = os.popen(utf8Str(s))
  realoutput = string.strip(string.join(o.readlines(),""))
  o.close()
  if useUnicode:
    return realoutput.decode('utf-8')
  else:
    return realoutput

for policy in getChildrenCalled("policy",policies):
  print "#####"
  print "# Testing:",testroot.getAttribute("purpose"),"("+policy.getAttribute("name")+")"
  print "#"

  try:
    prog = getChildCalled("program",policy).nodeValue
  except "NoSuchChild",name:
    prog = defaultProg

  if not os.access(prog,os.X_OK):
    raise "ProgramNotFoundOrNotExecutable",prog

  args = defaultArgs[:]
  opts = defaultOpts[:]

  for n in getChildrenCalled("addArg",policy):
    args.append(textValue(n))

  for n in getChildrenCalled("addOption",policy):
    opts.append(n.getAttribute("name"),textValue(n))

  if verbose:
    print "Program:",asciiStr(prog)
    print "Options:",asciiStr(string.join(opts," "))
    print "Arguments:",asciiStr(string.join(args," "))

  passed = 0
  skipped = 0
  failures = 0

  policyName = policy.getAttribute("name")

  failureLogName = "failures."+testroot.getAttribute("purpose")+"."+policyName
  failureLogName=string.replace(failureLogName," ","")
  failureLogName=string.replace(failureLogName,"/","")
  failureLog = open(failureLogName,"w")

  print "Running tests: ",

  for b in getChildrenCalled("base",testroot):
    type = b.getAttribute("type")
    if (testType == None) or (type == testType):
      arg = textValue(getChildCalled("arg",b))
      if unorderedMode == 1:
        arg = "unordered(" + arg + ")"
      if verbose >= 2:
        print "\nTest:",asciiStr(arg),"(type: "+type+") ",

      output = ''
      for o in getChildrenCalled("output", b):
        if (not o.hasAttribute("policy")) or (policyName == o.getAttribute("policy")):
          output = string.strip(textValue(o))
      
      if on_cygwin:
        # Kludge to allow for running of windows binaries using the cygwin copy of python
        output = string.replace(output,"$PWD$",string.replace(os.getcwd(),"cygdrive/c","c:"))
      else:
        output = string.replace(output,"$PWD$",os.getcwd()) 

      realoutput = runTest(prog,opts,args,arg)

      if on_cygwin:
        # Don't really understand why this is occuring
        realoutput = string.replace(realoutput,"\r","")
        output = string.replace(output,"\r","")
      
      if output==realoutput:
        passed = passed + 1
        sys.stdout.write(".")
        if type == "fail":
          # A fail type passed!
          failPasses.append(arg)
          b.setAttribute("type","pass")
      else:
        failures = failures + 1
        sys.stdout.write("!")
        if verbose >= 2:
          print "\nGot:",asciiStr(realoutput)
          print "Expected:",asciiStr(output)
        failureLog.write("Failed with input "+utf8Str(arg)+".\n")
        failureLog.write("Expected output:\n"+utf8Str(output)+"\n")
        failureLog.write("Actual output:\n"+utf8Str(realoutput)+"\n\n")
    else:
      skipped = skipped + 1
      if verbose:
        sys.stdout.write("x")
    sys.stdout.flush()
  print

  failureLog.close()

  print "Stats:",passed,"passed,",failures,"failed ("+str(failures)+"/"+str(failures+passed)+"),",skipped,"skipped"
  if failures > 0:
    print "More information:",failureLogName
    totalFailures = totalFailures + failures
  else:
    os.unlink(failureLogName)

if doAdd:
  #os.system("stty", '-icanon', 'eol', "\001") # set STDIN to one byte at a time  #os.system("stty", 'icanon', 'eol', '^@') # sets STDIN to one line at a time
  ans = ""
  while ans != "n":
    ans = string.lower(string.strip(raw_input("Do you want to add a test? (Y/N) ")))
    if ans == "y":
      arg = string.strip(raw_input("Argument to use: "))
      output = runTest(prog,opts,args,arg)
      print asciiStr(output)
      ok = string.lower(string.strip(raw_input("Is this ok? (Y/N)")))
      if ok == "y":
        base = testXML.createElement("base")
        base.setAttribute("type","pass")
        inp = testXML.createElement("arg")
        t = testXML.createTextNode(arg)
        inp.appendChild(t)
        base.appendChild(inp)

        out = testXML.createElement("output")
        t = testXML.createTextNode(output)
        out.appendChild(t)
        base.appendChild(out)
        testroot.appendChild(base)

  updateFile = 1

if len(failPasses)>0:
  print "--> Some tests marked as 'fail' passed:"
  for p in failPasses:
    print "   ",p
  print
  if not updateFile:
    print "Re-run with \"-u\" to update the policy file with these tests changed to 'pass'"

if updateFile:
  if useUnicode:
    f = codecs.open("tester.xml","w","utf-8")
  else:
    f = open("tester.xml","w")
  
  #testXML.writexml(f)
  #Kludge alert!
  f.write('<?xml version="1.0" encoding="utf-8"?>\n')
  
  for node in testXML.childNodes:
    node.writexml(f, "", "", "")

  f.close()
  print "XML file updated"

sys.exit(totalFailures) # Should probably take min(totalFailures,255)
