#
# This Python module defines a plugin that can be used by coopr_test_driver 
# (and related unittest drivers) to execute tests on PEBBL solvers.
#

import os
import os.path
import pyutilib.autotest
import coopr.opt
from pyutilib.component.core import *
import pyutilib.services
import re

#
# Register executables used in PEBBL tests
#
pyutilib.services.register_executable('knapsack')
pyutilib.services.register_executable('mpirun')


#
# Plugin that defines actions in the setUp, execution and tearDown
# of tests.
#
class PebblTestDriver(pyutilib.autotest.TestDriverBase):

    alias('acro.pebbl')

    def setUpClass(self, cls, options):
        """Set-up the class that defines the suite of tests"""

    def tearDownClass(self, cls, options):
        """Tear-down the class that defines the suite of tests"""

    def setUp(self, testcase, options):
        """Set-up a single test in the suite"""
        #
        # Set the current working directory
        #
        global tmpdir
        tmpdir = os.getcwd()
        os.chdir(options.currdir)
        #
        # Setup temporary file manager
        #
        pyutilib.services.TempfileManager.sequential_files(0)
        pyutilib.services.TempfileManager.tempdir = options.currdir
        #
        # Get the PEBBL solver specified in the experiment
        #
        testcase.opt =  pyutilib.services.registered_executable(options.solver)
        if testcase.opt is None:
            testcase.skipTest('Solver %s is not available' % options.solver)
        testcase.mpirun =  pyutilib.services.registered_executable('mpirun')
        if options.np and testcase.mpirun is None:
            testcase.skipTest('Cannot execute solver %s in parallel.  %s is not available' % (options.solver, 'mpirun'))

    def tearDown(self, testcase, options):
        """Tear-down a single test in the suite"""
        #
        # Delete temporary files and  
        #
        pyutilib.services.TempfileManager.clear_tempfiles()
        pyutilib.services.TempfileManager.unique_files()
        #
        # Move back to the original working directory
        #
        global tmpdir
        os.chdir(tmpdir)

    def run_test(self, testcase, name, options):
        """Execute a single test in the suite"""
        #
        # Debugging I/O
        #
        if options.verbose or options.debug:
            print "Test %s - Running PEBBL solver with options %s" % (name, str(options))
        #
        # Create the command-line
        #
        if options.np:
            cmd = testcase.mpirun.get_path()+" -np "+str(options.np)+" "+testcase.opt.get_path()
        else:
            cmd = testcase.opt.get_path()
        cmd += " --output "+name+".sol.txt"
        if not options.solver_options is None:
            cmd += " "+options.solver_options
        cmd += " "+options.data
        if options.verbose or options.debug:
            print "Command Line: ",cmd
        #
        # Execute the PEBBL command
        #
        try:
            pyutilib.subprocess.run(cmd, options.currdir+name+".log", cwd=options.currdir)
        except OSError:
            pass
        #
        # Parse the logfile
        #
        results = self.process_log(options.currdir+name+".log")
        #
        # Apply assertions
        #
        testcase.assertEqual(results.value, eval(options.optimum))
        testcase.assertLessEqual(results.valgrind_errors, 0)
        testcase.assertLessEqual(results.valgrind_leaks, 0)
        if options.np > 1:
           testcase.assertEqual(results.nprocessors,options.np)
        #
        # Misc cleanup
        #
        if os.path.exists(options.currdir+name+".log"):
            os.remove(options.currdir+name+".log")
        if os.path.exists(options.currdir+name+".sol.txt"):
            os.remove(options.currdir+name+".sol.txt")

    def process_log(self, logfile):
        """Process a logfile generated by a PEBBL solver"""
        results = pyutilib.misc.Options()
        results.status = 1
        enum=""
        leak_flag=False
        valgrind_leaks="-1"
        valgrind_errors="-1"
        if os.path.exists(logfile):
           INPUT = open(logfile,"r")
           #
           # Iterate through the logfile looking for lines that match 'interesting' patterns
           #
           for line in INPUT:
             #print "LINE",line
             words = re.split('[ \t]+',line.strip())
             if len(words) < 2:
                continue

             if len(words) > 3 and words[2] == "Value" and words[3] == "=":
                results.status = 0
                results.value = eval(words[4])

             elif len(words) == 2 and words[1] == "processors":
                results.nprocessors = eval(words[0])

             elif words[0] == "CPU" and words[1] == "RunTime=":
                results.cpu_runtime = eval(words[2])

             elif words[0] == "CPU" and words[1] == "TotalTime=":
                results.cpu_totaltime = eval(words[2])

             elif words[0] == "WallClock" and words[1] == "TotalTime=":
                results.wallclock_totaltime = eval(words[2])

             elif words[0] == "Created":
                results.subproblems_created = eval(words[1])

             elif words[0] == "Bounded":
                if words[1] != "during":
                   results.subproblems_bounded = eval(words[1])

             elif words[0] == "Split":
                results.subproblems_split = eval(words[1])

             elif words[0] == "Dead":
                results.subproblems_dead = eval(words[1])

             elif words[0] == "Started" and words[1] == "Bounding":
                results.subproblems_started_bounding = eval(words[2])

             elif words[0] == "Started" and words[1] == "Splitting":
                results.subproblems_started_splitting = eval(words[2])

             elif words[0] == "EnumerationCount:":
                print >>OUTPUT, "EnumerationCount numeric/integer",words[1]
                results.enumeration_count = eval(words[1])
             elif words[0] == "Value:" and words[2] == "Solution:":
                enum = enum + line

             elif words[0] == "Seed:":
                results.seed = eval("int(" + words[1] + ")")

             #
             # When running under AMPL, the first set of valgrind statistics is the
             # one's that we want
             #
             elif words[1] == "ERROR" and words[2] == "SUMMARY:" and valgrind_errors == "-1":
                valgrind_errors = words[3]
             elif leak_flag==True and words[1] == "definitely" and valgrind_leaks == "-1":
                valgrind_leaks = words[3]
                leak_flag=False

             elif words[1] == "LEAK" and words[2] == "SUMMARY:":
                leak_flag=True

        #
        # Remove commas from valgrind #'s
        #
        p = re.compile( ',' )
        valgrind_errors = p.sub( "", valgrind_errors)
        valgrind_leaks = p.sub( "", valgrind_leaks)
        results.valgrind_errors = eval(valgrind_errors)
        results.valgrind_leaks = eval(valgrind_leaks)

        if enum != "":
            results.enumerated_points = enum.strip()

        return results
