#!/usr/bin/env python3

##  Author: Gleb Belov@monash.edu  2017
##  This program runs MiniZinc solvers over a set of instances,
##  checks solutions and compares to given solution logs.

##  TODO type errors etc. when checking?
##  TODO continuous output dumping as option
##  TODO CPU/user time limit, proper memory limit (setrlimit not working)

import sys, io, re as regex, traceback
import os.path, platform
##import numpy
import math
import json, argparse
import datetime
from collections import OrderedDict

import utils, json_config, json_log, mzn_exec, cmp_result_logs
from json_config import s_CommentKey, s_AddKey

s_ProgramDescr = 'MiniZinc testing automation. (c) 2018 Monash University, gleb.belov@monash.edu'
s_ProgramDescrLong = ( "Allows checking of MiniZinc solutions by configurable checker profiles. The solutions can be input from previous logs or produced by a chosen solver profile, for 1 or several instances, with result comparison and some ranking. New solutions' summary logs, detailed stdout/err outputs, and statistics are saved in subfolder mzn-test/LOGS, /OUTPUTS, and /STATS, resp.")

###########################   GENERAL CONFIG. Could be in the JSON config actually    #########################
sDirResults = "mzn-test"
sFlnSolUnchk = sDirResults + "/sol__unchk.json"      ### Logfile to append immediate results
sFlnSolCheckBase = sDirResults + "/LOGS/sol__{}.json"        ### Logfile to append checked results. {} replaced by datetime
sFlnSolFailBase = sDirResults + "/LOGS/sol__{}__FAIL.json"      ### To save failed solutions
sFlnSolLastDzn = sDirResults + "/sol_last.dzn"     ### File to save the DZN solution for checking
sFlnStdoutBase = sDirResults + "/OUTPUTS/{}.stdout.txt"      ### File name base to dump stdout for any backend call
sFlnStderrBase = sDirResults + "/OUTPUTS/{}.stderr.txt"
sFlnStatLog = sDirResults + "/STATS/stat_log__{}.txt"              ### The instance list name(s) will be inserted, if any

sDZNOutputAgrs = "--output-mode dzn --output-objective"    ## The flattener arguments to produce DZN-compatible output facilitating solution checking
                             ## Remove --output-objective in MZN Release <=2.1.7
sFlatOptChecker = "--allow-multiple-assignments -Dmzn_ignore_symmetry_breaking_constraints=true"

s_UsageExamples = ( 
    "\nUSAGE EXAMPLES:"
    "\n(1)  \"mzn-test.py model.mzn data.dzn [--checkDZN stdout.txt [--checkStderr stderr.txt]] [--chkPrf MINIZINC-CHK --chkPrf FZN-GECODE-CHK] [--tCheck 15] [--addSolverOption \"--fzn-flags '-D fPureCircuit=true'\"]\"                  ::: check the instance's solutions, optionally reading them from a DZN-formatted file (otherwise solving first), optionally overriding default checker list etc."
    "\n(2)  \"mzn-test.py --solver CPLEX -t 300 -l instList1.txt -l instList2.txt --name CPLEXTest_003 --result newLog00.json prevLog1.json prevLog2.json --failed failLog.json\""
    "             ::: solve instances using the specified solver profile and wall time limit 300 seconds. The instances are taken from the list files. The test is aliased CPLEXTest_003. Results are saved to newLog00.json and compared/ranked to those in prevLog's. (Probably) incorrect solutions are saved to failLog.json."
    "\n(3)  \"mzn-test.py [-l instList1.txt] -c prevLog1.json -c prevLog2.json [--runAndCmp]\"                  ::: compare existing logs, optionally limited to the given instances, optionally running new tests. USE SINGLE QUOTES ONLY INSIDE ARGUMENTS PASSED TO THE BACKENDS when running backends through shell."
  )
##############################################################################################
################ Parameters of MZN-Test, including config and command-line
##############################################################################################
class MZT_Param:
    def parseCmdLine(self):
        parser = argparse.ArgumentParser(
          description=s_ProgramDescr + '\n' + s_ProgramDescrLong,
          epilog=s_UsageExamples)
        parser.add_argument('instanceFiles', nargs='*', metavar='<instanceFile>',
          help='model instance files, if no instance lists supplied, otherwise existing solution logs to compare with')
        parser.add_argument('-l', '--instanceList', dest='l_InstLists', action='append', metavar='<instanceListFile>',
            help='file with a list of instance input files, one instance per line,'
              ' instance file types specified in config. Used for running tests or for instance selection in comparison mode')
        parser.add_argument('--name', '--testName', metavar='<string>', help='alias of this test run, defaults to result log file name')
        parser.add_argument('-c', '--compare', action="append", metavar='<logfile>',
                            help='summarize and compare results to existing <logfile>. Only compares the logs and does not run tests, unless --runAndCmp. The flags -c can be omitted if -l is used')
        parser.add_argument('--runAndCmp', '--runAndCompare', '--run', action='store_true',
                            help='even if other logs are provided by -c, do run the tests and compare')
        parser.add_argument('--solver', '--solverPrf', '--solverProfile', metavar='<prf_name>',
                            help='solver profile from those defined in config section \"SOLVER_PROFILES\"')
        parser.add_argument('--call', '--solverCall', metavar='"<exe+flags or shell command(s) if --shellSolve 1>"',
                            help='solver backend call, should be quoted. Insert %%s where instance files need to be. Flatten with \''
                             + sDZNOutputAgrs + '\' to enable solution checking, unless the model has a suitable output definition.'
                             " Pass '--output-time' to the output filter (e.g., solns2out) to enable ranking by time")
        parser.add_argument('-t', '--tSolve',
                            type=float,
                            metavar='<sec>', help='solver backend wall-time limit, default: '+
                              str(self.cfgDefault["BACKEND_DEFS"]["__BE_SOLVER"]["EXE"]["n_TimeoutRealHard"][0]))
        parser.add_argument('--result', default=sFlnSolCheckBase, metavar='<file>',
                            help='save result log to <file>, default: \''+sFlnSolCheckBase+'\'')
        parser.add_argument('--resultUnchk', default=sFlnSolUnchk, metavar='<file>', help='save unchecked result log to <file>')
        parser.add_argument('--chkPrf', '--checkerPrf', '--checkerProfile', metavar='<prf_name>', action='append',
                            help='checker profile from those defined in config section \"CHECKER_PROFILES\", can be a few')
        parser.add_argument('--tCheck',
                            type=float,
                            metavar='<sec>', help='checker backend wall-time limit, default: '+
                              str(self.cfgDefault["BACKEND_DEFS"]["__BE_CHECKER"]["EXE"]["n_TimeoutRealHard"][0]))
        parser.add_argument('--nCheckMax', '--nCheck', '--nCheckedMax', ## default=self.cfgDefault["COMMON_OPTIONS"]["SOLUTION_CHECKING"]["s_FailedSaveFile"][0],
                            type=int,
                            metavar='<N>', help='max number of solutions checked per instance.'
                              ' Negative means checking starts from the last obtained solution. Default: '+
                              str(self.cfgDefault["COMMON_OPTIONS"]["SOLUTION_CHECKING"]["n_CheckedMax"][0]))
        parser.add_argument('--nFailedSaveMax', ## default=self.cfgDefault["COMMON_OPTIONS"]["SOLUTION_CHECKING"]["s_FailedSaveFile"][0],
                            type=int,
                            metavar='<N>', help='max number of failed solution reports saved per instance, default: '+
                              str(self.cfgDefault["COMMON_OPTIONS"]["SOLUTION_CHECKING"]["n_FailedSaveMax"][0]))
        parser.add_argument('--failed', ## default=self.cfgDefault["COMMON_OPTIONS"]["SOLUTION_CHECKING"]["s_FailedSaveFile"][0],
                            metavar='<file>', help='save failed check reports to <file>, default: \''+
                              self.cfgDefault["COMMON_OPTIONS"]["SOLUTION_CHECKING"]["s_FailedSaveFile"][0]+'\'')
        parser.add_argument('--checkDZN', '--checkStdout', metavar='<stdout_file>',
                            help='for a single instance, check DZN-formatted solutions from a solver\'s std output dumped to <stdout_file>. The DZN format is produced, e.g., if the model is flattened with \'' + sDZNOutputAgrs + '\'')
        parser.add_argument('--checkStderr', metavar='<stderr_file>',
                            help='with --checkDZN, read a solver\'s stderr log from <stderr_file> (not essential)')
        parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', help='tee backend\'s stderr to screen, in addition to the instance\'s output dumpfile in mzn-test/OUTPUTS. Only works for --shellSolve 1')
        parser.add_argument('--vc', '--verbose-check', dest='vc', action='store_true', help='same for checking')
     ## parser.add_argument('--no-feature', dest='feature', action='store_false')
     ## parser.set_defaults(feature=True)
        parser.add_argument('--debug', '--printcall', type=int, metavar='<bitfield>', help='bit 1: print full solver call commands, bit 2: same for checker')
        parser.add_argument('--shellSolve', type=int, metavar='0/1', help='backend call through shell when using psutils')
        parser.add_argument('--psutils', type=int, metavar='0/1', help='backend call through psutils (seems buggy in 3.4.2)')
        ## parser.add_argument('--fullPaths', action='store_true',
        ##                    help='use full paths in instance identifiers. By default, it\'s the pure base filenames')
        
        parser.add_argument('--mergeCfg', action="append", metavar='<file>', help='merge config from <file>')
        parser.add_argument('--saveCfg', metavar='<file>', help='save internal config to <file>. Can be useful to modify some parameters and run with --mergeCfg')
        parser.add_argument('--saveSolverCfg', metavar='<file>', help='save the final solver backend config to <file>')
        parser.add_argument('--saveCheckerCfg', metavar='<file>', help='save the final checker backend config to <file>')
        parser.add_argument('--addOption', '--addOptions', action="append", metavar='<text>', type=str, help='add <text> to any solver / checker call')
        parser.add_argument('--addSolverOption', '--addSolverOptions', action="append", metavar='<text>', type=str, help='add <text> to the solver call')
        parser.add_argument('--addCheckerOption', '--addCheckerOptions', action="append", metavar='<text>', type=str, help='add <text> to a checker call')
        parser.add_argument('--useJoinedName', action="append", metavar='<...%s...>', type=str, help='add this to the call, with %%s being replaced by'
                            ' a joined filename from all the input filenames, e.g., "--writeModel MODELS/%%s.mps"')
        self.args = parser.parse_args()
        # print( "ARGS:\n", self.args )
        ## Solver backend and checker backend list
        self.slvBE = None
        self.chkBEs = None

    ## Get parameters from config and command line, merge values
    def obtainParams(self):
        self.parseCmdLine()
        self.mergeValues()
        
    def initCfgDefault(self):
        ddd = {
          s_CommentKey: [
            "The default config structure for mzn-test.py. ", s_ProgramDescr,
            "You can export this by --saveCfg and modify -> --mergeCfg,",
            "even having only partial JSON subtree in a merged file(s).",
            "Structure: COMMON_OPTIONS, SOLVER_/CHECKER_PROFILES, BACKEND_DEFS.",
            "Solver and checkers are selected from pre-defined profiles,",
            "which are in turn built from sequences of 'basic backend definitions'.",
            "Comments are either separate keys or added as list elements ('/// ...')",
            "in pre-selected positions; then, overriding items should keep that order."
          ],
          "COMMON_OPTIONS": {
            s_CommentKey: [ "'Solvers' and 'Checkers' select the profiles to use for solving and checking.",
                            "At the moment only the 1st solver is used from Solvers."
                            "The selected profiles must be known in SOLVER_PROFILES / CHECKER_PROFILES, resp."
                          ],
            "Solvers": [ "MINIZINC",
                        #"MZN-CPLEX",
                        "/// At the moment only the 1st element is used for solving" ],
            "SOLUTION_CHECKING": {
              "Checkers": ["GECODE-CHK", "GUROBI-CHK", "CPLEX-CHK", "ORTOOLS-CHK" ],
              "n_CheckedMax": [ -10, "/// Negative value means it's that many last solutions" ],
              "n_FailedSaveMax": [ 3, "/// After that many failed solutions, stop checking the instance" ],
              "s_FailedSaveFile": [ sFlnSolFailBase, "/// Filename to save failed solutions" ],
            },
            "Instance_List": {
              s_CommentKey: [ "Params for instance lists.",
                              "Instance list is a file containing an instance's model files,",
                              "at most 1 instance per line.",
                              "InstanceFileExt: only files with this extensions from a list file",
                              "will be taken on each line" ],
              "InstanceFileExt": [".mzn", ".dzn"]     ##   Add json?   TODO
            },
            "runCommand": {
              "windows": {
                "runSilent": "echo \"WARNING. No timeout on Windows.\" & {2} 1>{3} 2>{4}",
                "runVerbose": "echo \"WARNING. No timeout on Windows.\" & {2} 3>&1 1>{3} 2>&3 | tee {4} & echo >>{4}"
              }
              ,"non-windows": {
                "runSilent": "ulimit -v {0}; timeout -k 1 {1} bash -c \"{2}\" 1>{3} 2>{4}",
                "runVerbose": "ulimit -v {0}; timeout -k 1 {1} bash -c \"{2}\" 3>&1 1>{3} 2>&3 | tee {4}; echo >>{4}"
              }
            }
          },
          "SOLVER_PROFILES": {
            s_CommentKey: [ "Similar to CHECKER_PROFILES." ],
            "MINIZINC": [ "__BE_COMMON", "__BE_SOLVER", "BE_MINIZINC" ],
            "FZN-GUROBI": [ "__BE_COMMON", "__BE_SOLVER", "BE_FZN-GUROBI" ],
            "FZN-CPLEX": [ "__BE_COMMON", "__BE_SOLVER", "BE_FZN-CPLEX" ],
            "FZN-CBC": [ "__BE_COMMON", "__BE_SOLVER", "BE_FZN-CBC" ],
            "GUROBI": [ "__BE_COMMON", "__BE_SOLVER", "BE_GUROBI" ],
            "CPLEX": [ "__BE_COMMON", "__BE_SOLVER", "BE_CPLEX" ],
            "XPRESS": [ "__BE_COMMON", "__BE_SOLVER", "BE_XPRESS" ],
            "SCIP": [ "__BE_COMMON", "__BE_SOLVER", "BE_SCIP" ],
            "CBC": [ "__BE_COMMON", "__BE_SOLVER", "BE_CBC" ],
            "GECODE": [ "__BE_COMMON", "__BE_SOLVER", "BE_GECODE" ],
            "CHUFFED": [ "__BE_COMMON", "__BE_SOLVER", "BE_CHUFFED" ],
            "ORTOOLS": [ "__BE_COMMON", "__BE_SOLVER", "BE_ORTOOLS" ] #,
            # "MZN-GUROBI": [ "__BE_COMMON", "__BE_SOLVER", "BE_MZN-GUROBI" ],
            # "MZN-CPLEX": [ "__BE_COMMON", "__BE_SOLVER", "BE_MZN-CPLEX" ],
            # "MZN-CBC": [ "__BE_COMMON", "__BE_SOLVER", "BE_MZN-CBC" ],
            # "MZN-GECODE": [ "__BE_COMMON", "__BE_SOLVER", "BE_MZN-GECODE" ],
            # "FZN-CHUFFED": [ "__BE_COMMON", "__BE_SOLVER", "BE_FZN-CHUFFED" ]
          },
          "CHECKER_PROFILES": {
            s_CommentKey: [ "Each profile gives a list of backend defs to use.",
              "Later backends in the list can override/add options, ",
              "for example for values to be read from the outputs.",
              "Adding is only possible if the key is prefixed by '"+s_AddKey+"'"
            ],
            "MINIZINC-CHK": [ "__BE_COMMON", "__BE_CHECKER_OLDMINIZINC", "BE_MINIZINC" ],
            "GECODE-CHK": [ "__BE_COMMON", "__BE_CHECKER", "BE_GECODE" ],
            "ORTOOLS-CHK": [ "__BE_COMMON", "__BE_CHECKER", "BE_ORTOOLS" ],
            "GUROBI-CHK": [ "__BE_COMMON", "__BE_CHECKER", "BE_GUROBI" ],
            "CPLEX-CHK": [ "__BE_COMMON", "__BE_CHECKER", "BE_CPLEX" ],
            "CHUFFED-CHK": [ "__BE_COMMON", "__BE_CHECKER", "BE_CHUFFED" ],
            "MZN-GECODE-CHK": [ "__BE_COMMON", "__BE_CHECKER", "BE_MZN-GECODE" ],
            "MZN-GUROBI-CHK": [ "__BE_COMMON", "__BE_CHECKER", "BE_MZN-GUROBI" ],
            "MZN-CPLEX-CHK": [ "__BE_COMMON", "__BE_CHECKER", "BE_MZN-CPLEX" ],
            "FZN-GECODE-CHK": [ "__BE_COMMON", "__BE_CHECKER_OLDMINIZINC", "BE_FZN-GECODE" ],
            "FZN-GECODE-SHELL-CHK": [ "__BE_COMMON", "__BE_CHECKER_OLDMINIZINC", "BE_FZN-GECODE_SHELL" ],
            "FZN-CHUFFED-CHK": [ "__BE_COMMON", "__BE_CHECKER", "BE_FZN-CHUFFED" ]
          },
          "BACKEND_DEFS": {
            s_CommentKey: [ "__BE_COMMON initializes a basic backend structure.",
              "Each further backend in a profile list overrides or adds options."
            ],
            "__BE_COMMON": {
              s_CommentKey: [ "THE INITIALIZING BACKEND." ],
              "EXE": {
                s_CommentKey: [ "Solver call parameters" ],
                "s_SolverCall" : ["minizinc -v -s -i " + sDZNOutputAgrs + " %s",
                  "/// The 1st element defines the call line. %s is replaced by the instance filename(s)."],
                "s_ExtraCmdline" : ["", "/// Only for __BE_SOLVER/__BE_CHECKER... subprofiles."
                                        " The 1st element gives extra cmdline arguments to the call"],
                "b_ThruShell"  : [True, "/// Set True to call solver thru shell."
                  " Then you can do shell tricks but Ctrl+C may not kill all subprocesses etc."],
                "n_TimeoutRealHard": [150, "/// Real-time timeout per instance, seconds,"
                  " for all solution steps together. Use mzn/backend options for CPU time limit."],
                "n_VMEMLIMIT_SoftHard": [8000000, 8000000, "/// 2 limits, soft/hard, in KB. Platform-dependent in Python 3.6. Default 8 GB"],
              },
              "Stderr_Keylines": {
                s_CommentKey: [ "A complete line in stderr will be interpreted accordingly.",
                  " Format: <outvar> : { <line>: <value>, ... }"
                  " You can add own things here (use '"+s_AddKey+"' before new var name)",
                  " which will be transferred into results" ]
              },
              "Stderr_Keyvalues": {
                s_CommentKey: [ "Numerical values to be extracted from a line in stderr.",
                  " { <outvar>: [ <regex search pattern>, <regex to replace by spaces>, <value's pos in the line>] }."
                ], 
                ### The %%%mzn-stat values appear in stdout (as of May 2019) but leave them here just in case
                "Time_Flt": [ "%%%mzn-stat: flatTime", "[:=]", 3, "/// E.g., 'Flattening done, 3s' produces 3."
                                " !!! This is interpreted as successful flattening by the checker" ],
                "ObjVal_Solver":   [ "%%%mzn-stat: objective=", "[,:/=]", 3,        ## Need = to avoid mixup with the bound
                                        "/// The objval as reported by solver."],
                "DualBnd_Solver":   [ "%%%mzn-stat: objectiveBound", "[,:/=]", 3 ],
                "CPUTime_Solver":   [ "%%%mzn-stat: solveTime", "[,:/=]", 3 ],
                "NNodes_Solver":   [ "%%%mzn-stat: nodes", "[,:/=]", 3 ],
              },
              "Stdout_Keylines": {
                s_CommentKey: [ "Similar to Stderr_Keylines"],
                "Sol_Status": {
                  "----------": 1,
                  "==========": 2,
                  "=====UNSATISFIABLE=====": -1,
                  "=====UNBOUNDED=====": -2,
                  "=====UNKNOWN=====": 0,
                  "=====UNSATorUNBOUNDED=====": -3,
                  "=====ERROR=====": -4
                },
                "Problem_Sense": {
                  "%%%mzn-stat: method=\"maximize\"": 1,
                  "%%%mzn-stat: method=\"minimize\"": -1,
                  "%%%mzn-stat: method=\"satisfy\"": 0,
                }
              },
              "Stdout_Keyvalues": {
                s_CommentKey: ["Similar to Stderr_Keyvalues." ],
                "Time_Flt": [ "%%%mzn-stat: flatTime", "[:=]", 3, "/// E.g., 'Flattening done, 3s' produces 3."
                                " !!! This is interpreted as successful flattening by the checker" ],
                "ObjVal_Solver":   [ "%%%mzn-stat: objective=", "[,:/=]", 3,        ## Need = to avoid mixup with the bound
                                        "/// The objval as reported by solver."],
                "DualBnd_Solver":   [ "%%%mzn-stat: objectiveBound", "[,:/=]", 3 ],
                "CPUTime_Solver":   [ "%%%mzn-stat: solveTime", "[,:/=]", 3 ],
                "NNodes_Solver":   [ "%%%mzn-stat: nodes", "[,:/=]", 3 ],
                "ObjVal_MZN":   [ "_objective", "[():=;%]", 2,
                                    "/// The objective value as evaluated by MZN." ],
                "RealTime_Solns2Out": [ "% time elapsed:", " ", 4 ],
              }
            },
            "__BE_SOLVER": {
              s_CommentKey: ["Specializations for a general solver" ],
              "EXE": {
                "s_ExtraCmdline" : ["-i"],
                "b_ThruShell"  : [True],
                "n_TimeoutRealHard": [150],
                #  "n_VMEMLIMIT_SoftHard": [8000100000, 8100000000]
              }
            },
            "__BE_CHECKER": {
              s_CommentKey: ["Specializations for a general checker" ],
              "EXE": {
                "s_ExtraCmdline" : [sFlatOptChecker],
                "b_ThruShell"  : [True],
                "n_TimeoutRealHard": [15],
                #  "n_VMEMLIMIT_SoftHard": [8100000000, 8100000000]
              }
            },
            "__BE_CHECKER_OLDMINIZINC": {
              s_CommentKey: ["Specializations for a general checker using the 1.6 MiniZinc driver" ],
              "EXE": {
                "s_ExtraCmdline" : ["--mzn2fzn-cmd 'mzn2fzn -v -s --output-mode dzn " + sFlatOptChecker + "'"],
                "b_ThruShell"  : [True],
                "n_TimeoutRealHard": [15],
                #  "n_VMEMLIMIT_SoftHard": [8100000000, 8100000000]
              }
            },
            "BE_MINIZINC": {
              s_CommentKey: [ "------------------- Specializations for pure minizinc driver" ],
              "EXE":{
                "s_SolverCall": [ "minizinc --mzn2fzn-cmd 'mzn2fzn -v -s " + sDZNOutputAgrs + "' -s %s"], # _objective fails for checking
                "b_ThruShell"  : [True],
              },
            },
            "BE_FZN-GUROBI": {
              s_CommentKey: [ "------------------- Specializations for Gurobi solver instance" ],
              "EXE":{
                "s_SolverCall" : ["mzn2fzn -v -s -G linear " + sDZNOutputAgrs + " %s --fzn tmp.fzn --ozn tmp.ozn && mzn-gurobi -v -s tmp.fzn"],  ## works without solns2out for now. Need thus when using shell call with system()  TODO?
                #"opt_writeModel": ["--writeModel"]
              },
              "Stderr_Keyvalues": {
                s_AddKey+"Preslv_Rows": [ "Presolved:", " ", 2 ],
                s_AddKey+"Preslv_Cols": [ "Presolved:", " ", 4 ],
                s_AddKey+"Preslv_Non0": [ "Presolved:", " ", 6 ]
              },
            },
            "BE_GUROBI": {
              s_CommentKey: [ "------------------- Specializations for Gurobi solver instance" ],
              "EXE":{
                "s_SolverCall" : ["minizinc -v -s --solver gurobi --output-time " + sDZNOutputAgrs + " %s"], # _objective fails for checking TODO
                #"opt_writeModel": ["--writeModel"]
              },
              "Stderr_Keyvalues": {
                s_AddKey+"Preslv_Rows": [ "Presolved:", " ", 2 ],
                s_AddKey+"Preslv_Cols": [ "Presolved:", " ", 4 ],
                s_AddKey+"Preslv_Non0": [ "Presolved:", " ", 6 ]
              },
            },
            "BE_MZN-GUROBI": {
              s_CommentKey: [ "------------------- Specializations for Gurobi solver instance" ],
              "EXE":{
                "s_SolverCall" : ["mzn-gurobi -v -s -G linear --output-time " + sDZNOutputAgrs + " %s"], # _objective fails for checking TODO
                #"opt_writeModel": ["--writeModel"]
              },
              "Stderr_Keyvalues": {
                s_AddKey+"Preslv_Rows": [ "Presolved:", " ", 2 ],
                s_AddKey+"Preslv_Cols": [ "Presolved:", " ", 4 ],
                s_AddKey+"Preslv_Non0": [ "Presolved:", " ", 6 ]
              },
            },
            "BE_FZN-CPLEX": {
              s_CommentKey: [ "------------------- Specializations for IBM ILOG CPLEX solver instance" ],
              "EXE": {
                "s_SolverCall" : ["mzn2fzn -v -s -G linear " + sDZNOutputAgrs + " %s --fzn tmp.fzn --ozn tmp.ozn && mzn-cplex -v -s tmp.fzn"]
                #"s_SolverCall" : ["./run-mzn-cplex.sh %s"],
                #"b_ThruShell"  : [True],
                #"opt_writeModel": ["--writeModel"]
              },
              "Stderr_Keyvalues": {
                s_AddKey+"Preslv_Rows": [ "Reduced MIP has [0-9]+ rows,", " ", 4 ],
                s_AddKey+"Preslv_Cols": [ "Reduced MIP has [0-9]+ rows,", " ", 6 ],
                s_AddKey+"Preslv_Non0": [ "Reduced MIP has [0-9]+ rows,", " ", 9 ]
              },
            },
            "BE_CPLEX": {
              s_CommentKey: [ "------------------- Specializations for IBM ILOG CPLEX solver instance" ],
              "EXE": {
                "s_SolverCall" : ["minizinc -v -s --solver cplex --output-time " + sDZNOutputAgrs + " %s"], # _objective fails for checking
                #"s_SolverCall" : ["./run-mzn-cplex.sh %s"],
                #"b_ThruShell"  : [True],
                #"opt_writeModel": ["--writeModel"]
              },
              "Stderr_Keyvalues": {
                s_AddKey+"Preslv_Rows": [ "Reduced MIP has [0-9]+ rows,", " ", 4 ],
                s_AddKey+"Preslv_Cols": [ "Reduced MIP has [0-9]+ rows,", " ", 6 ],
                s_AddKey+"Preslv_Non0": [ "Reduced MIP has [0-9]+ rows,", " ", 9 ]
              },
            },
            "BE_XPRESS": {
              s_CommentKey: [ "------------------- Specializations for FICO XPRESS solver instance" ],
              "EXE": {
                "s_SolverCall" : ["minizinc -v -s --solver xpress --output-time " + sDZNOutputAgrs + " %s"], # _objective fails for checking
                #"s_SolverCall" : ["./run-mzn-cplex.sh %s"],
                #"b_ThruShell"  : [True],
                #"opt_writeModel": ["--writeModel"]
              },
              "Stderr_Keyvalues": {   ## Is different for XPRESS and -v fails at the moment anyway
                s_AddKey+"Preslv_Rows": [ "Reduced MIP has [0-9]+ rows,", " ", 4 ],
                s_AddKey+"Preslv_Cols": [ "Reduced MIP has [0-9]+ rows,", " ", 6 ],
                s_AddKey+"Preslv_Non0": [ "Reduced MIP has [0-9]+ rows,", " ", 9 ]
              },
            },
            "BE_MZN-CPLEX": {
              s_CommentKey: [ "------------------- Specializations for IBM ILOG CPLEX solver instance" ],
              "EXE": {
                "s_SolverCall" : ["mzn-cplex -v -s -G linear --output-time " + sDZNOutputAgrs + " %s"], # _objective fails for checking
                #"s_SolverCall" : ["./run-mzn-cplex.sh %s"],
                #"b_ThruShell"  : [True],
                #"opt_writeModel": ["--writeModel"]
              },
              "Stderr_Keyvalues": {
                s_AddKey+"Preslv_Rows": [ "Reduced MIP has [0-9]+ rows,", " ", 4 ],
                s_AddKey+"Preslv_Cols": [ "Reduced MIP has [0-9]+ rows,", " ", 6 ],
                s_AddKey+"Preslv_Non0": [ "Reduced MIP has [0-9]+ rows,", " ", 9 ]
              },
            },
            "BE_FZN-CBC": {
              s_CommentKey: [ "------------------- Specializations for COIN-OR Branch&Cut solver instance" ],
              "EXE": {
                "s_SolverCall" : ["mzn-cbc -v -s -G linear --output-time " + sDZNOutputAgrs + " %s --fzn tmp.fzn --ozn tmp.ozn && mzn-cplex -v -s tmp.fzn"], 
                #"s_SolverCall" : ["./run-mzn-cplex.sh %s"],
                #"b_ThruShell"  : [True],
              },
            },
            "BE_SCIP": {
              s_CommentKey: [ "------------------- Specializations for SCIP solver instance" ],
              "EXE": {
                "s_SolverCall" : ["minizinc -v -s --solver scip --output-time " + sDZNOutputAgrs + " %s"], # _objective fails for checking
                #"s_SolverCall" : ["./run-mzn-cplex.sh %s"],
                #"b_ThruShell"  : [True],
              },
            },
            "BE_CBC": {
              s_CommentKey: [ "------------------- Specializations for COIN-OR Branch&Cut solver instance" ],
              "EXE": {
                "s_SolverCall" : ["minizinc -v -s --solver osicbc --output-time " + sDZNOutputAgrs + " %s"], # _objective fails for checking
                #"s_SolverCall" : ["./run-mzn-cplex.sh %s"],
                #"b_ThruShell"  : [True],
              },
            },
            "BE_MZN-CBC": {
              s_CommentKey: [ "------------------- Specializations for COIN-OR Branch&Cut solver instance" ],
              "EXE": {
                "s_SolverCall" : ["mzn-cbc -v -s -G linear --output-time " + sDZNOutputAgrs + " %s"], # _objective fails for checking
                #"s_SolverCall" : ["./run-mzn-cplex.sh %s"],
                #"b_ThruShell"  : [True],
              },
            },
            "BE_GECODE": {
              s_CommentKey: [ "------------------- Specializations for Gecode FlatZinc interpreter" ],
              "EXE": {
#                "s_SolverCall" : ["minizinc -s --solver gecode " + sDZNOutputAgrs + " %s"], # _objective fails for checking TODO
                "s_SolverCall" : ["minizinc -v -s --solver gecode " + sDZNOutputAgrs
                     + " %s"],    #  --time 300000
                "b_ThruShell"  : [True],
              }
            },
            "BE_MZN-GECODE": {
              s_CommentKey: [ "------------------- Specializations for Gecode FlatZinc interpreter" ],
              "EXE": {
#                "s_SolverCall" : ["mzn-fzn -s -G gecode --solver fzn-gecode " + sDZNOutputAgrs + " %s"], # _objective fails for checking TODO
                "s_SolverCall" : ["mzn-gecode -v -s -G gecode " + sDZNOutputAgrs
                     + " %s"],    #  --time 300000
                "b_ThruShell"  : [True],
              }
            },
            "BE_FZN-GECODE": {
              s_CommentKey: [ "------------------- Specializations for Gecode FlatZinc interpreter" ],
              "EXE": {
#                "s_SolverCall" : ["mzn-fzn -s -G gecode --solver fzn-gecode " + sDZNOutputAgrs + " %s"], # _objective fails for checking TODO
                "s_SolverCall" : ["minizinc -s -G gecode -f fzn-gecode --mzn2fzn-cmd 'mzn2fzn -v -s " + sDZNOutputAgrs + "' %s"],
                "b_ThruShell"  : [True],
              }
            },
            "BE_FZN-GECODE_SHELL": {
              s_CommentKey: [ "------------------- Specializations for Gecode FlatZinc interpreter" ],
              "EXE": {
#                "s_SolverCall" : ["mzn-fzn -s -G gecode --solver fzn-gecode " + sDZNOutputAgrs + " %s"], # _objective fails for checking TODO
                "s_SolverCall" : ["mzn2fzn -v -s -G gecode " + sDZNOutputAgrs
                     + sFlatOptChecker + " %s --fzn tmp.fzn --ozn tmp.ozn && fzn-gecode tmp.fzn | solns2out tmp.ozn"],
                "b_ThruShell"  : [True],
                "s_ExtraCmdline" : [""],
              }
            },
            "BE_CHUFFED": {
              s_CommentKey: [ "------------------- Specializations for Chuffed FlatZinc interpreter" ],
              "EXE": {
                "s_SolverCall" : ["minizinc -v -s --solver chuffed -f --output-time "
                                    + sDZNOutputAgrs + " %s"], # _objective fails for checking
              }  ##  --fzn-flags --time-out --fzn-flags 300
            }
            , "BE_ORTOOLS": {
              s_CommentKey: [ "------------------- Specializations for OR-Tools FlatZinc interpreter" ],
              "EXE": {
                "s_SolverCall" : ["minizinc -v -s --solver ortools -f --output-time "
                                    + sDZNOutputAgrs + " %s"], # _objective fails for checking
              }  ##  --fzn-flags --time-out --fzn-flags 300
            }
            , "BE_FZN-CHUFFED": {
              s_CommentKey: [ "------------------- Specializations for Chuffed FlatZinc interpreter" ],
              "EXE": {
                "s_SolverCall" : ["mzn-fzn -v -s -G chuffed --solver fzn-chuffed --fzn-flags -f --output-time "
                                    + sDZNOutputAgrs + " %s"], # _objective fails for checking
              }  ##  --fzn-flags --time-out --fzn-flags 300
            }
          }
        }
        return ddd
    ##        self.nNoOptAndAtLeast2Feas = 0
    
    ## Read a cfg file, instead of/in addition to the default cfg
    def mergeCfg(self, fln):
        ddd1 = None
        with open( fln, 'r' ) as rf:
            ddd1 = json.load( rf )
        self.cfg = mergeJSON( self.cfg, ddd1 )
      
    ## Merge cmdline values with cfg, performing some immediate actions
    ## And compile the backends constituting solver and checker(s)
    def mergeValues(self):
        if None!=self.args.mergeCfg:               ### MERGE CFG FROM EXTRA FILES
            for eCfg in self.args.mergeCfg:
                self.mergeCfg( eCfg )
        ################ Update some explicit cmdline params -- AFTER MERGING CFG FILES.
        if None!=self.args.failed:
            self.cfg["COMMON_OPTIONS"]["SOLUTION_CHECKING"]["s_FailedSaveFile"][0] = self.args.failed
        if None!=self.args.nCheckMax:
            self.cfg["COMMON_OPTIONS"]["SOLUTION_CHECKING"]["n_CheckedMax"][0] = self.args.nCheckMax
        if None!=self.args.nFailedSaveMax:
            self.cfg["COMMON_OPTIONS"]["SOLUTION_CHECKING"]["n_FailedSaveMax"][0] = self.args.nFailedSaveMax
        ################ SAVE FINAL CFG
        if None!=self.args.saveCfg:
            with utils.openFile_autoDir( self.args.saveCfg, 'w' ) as wf:
                print( "Saving final config to", self.args.saveCfg )
                json.dump( self.cfg, wf, sort_keys=True, indent=json_config.n_JSON_Indent )
        ### COMPILE THE SOLVER BACKEND
        if None!=self.args.solver:
            self.cfg["COMMON_OPTIONS"]["Solvers"][0] = self.args.solver
        slvPrfName = self.cfg["COMMON_OPTIONS"]["Solvers"][0]
        slvPrf = self.cfg["SOLVER_PROFILES"][slvPrfName]
        assert len(slvPrf)>0, "Solver profile '%s' should use at least a basic backend" % slvPrfName
        self.slvBE = self.cfg["BACKEND_DEFS"][slvPrf[0]]
        for i in range( 1, len( slvPrf ) ):
            self.slvBE = json_config.mergeJSON( self.slvBE, self.cfg["BACKEND_DEFS"][slvPrf[i]] )
        if None!=self.args.tSolve:
            self.slvBE["EXE"]["n_TimeoutRealHard"][0] = self.args.tSolve
        assert None==self.args.solver or None==self.args.call, "ERROR: both solver call and a solver profile specified."
        if None!=self.args.call:    ## After the compilation
            self.slvBE["EXE"]["s_SolverCall"][0] = self.args.call
        if None!=self.args.shellSolve:
            self.slvBE["EXE"]["b_ThruShell"][0] = self.args.shellSolve!=0
        print ( "\nSolver/checker configurations:\n     SLV_CFG: ", json.dumps( self.slvBE["EXE"] ) )
        ### COMPILE THE CHECKER BACKENDS
        if None!=self.args.chkPrf and 0<len( self.args.chkPrf ):
            self.cfg["COMMON_OPTIONS"]["SOLUTION_CHECKING"]["Checkers"] = self.args.chkPrf
        chkPrfList = self.cfg["COMMON_OPTIONS"]["SOLUTION_CHECKING"]["Checkers"]
        self.chkBEs = []
        for chkPrfName in chkPrfList:
            chkPrf = self.cfg["CHECKER_PROFILES"][chkPrfName]
            assert len(chkPrf)>0, "Checker profile '%s' should use at least a basic backend" % chkPrfName
            self.chkBEs.append( self.cfg["BACKEND_DEFS"][chkPrf[0]] )
            for i in range( 1, len( chkPrf ) ):
                self.chkBEs[-1] = json_config.mergeJSON( self.chkBEs[-1], self.cfg["BACKEND_DEFS"][chkPrf[i]] )
            if None!=self.args.tCheck:
                self.chkBEs[-1]["EXE"]["n_TimeoutRealHard"][0] = self.args.tCheck
            print ( "     CHK_CFG: ", json.dumps( self.chkBEs[-1]["EXE"] ) )
        print( "" )
        ### SAVE THE SOLVER BACKEND
        if None!=self.args.saveSolverCfg:
            with utils.openFile_autoDir( self.args.saveSolverCfg, 'w' ) as wf:
                print( "Saving solver config to", self.args.saveSolverCfg )
                json.dump( self.slvBE, wf, sort_keys=True, indent=json_config.n_JSON_Indent )
        ### SAVE THE CHECKER BACKENDS
        if None!=self.args.saveCheckerCfg:
            with utils.openFile_autoDir( self.args.saveCheckerCfg, 'w' ) as wf:
                print( "Saving checker config to", self.args.saveCheckerCfg )
                json.dump( self.chkBE, wf, sort_keys=True, indent=json_config.n_JSON_Indent )
        self.sThisName = self.args.result
        if None!=self.args.name:
            self.sThisName = self.args.name
    
    def __init__(self):
        self.cfgDefault = self.initCfgDefault()
        self.cfg = self.cfgDefault
        ### Further parameters
        self.args = {}
        
    def __str__(self):
        s_Out = json.dumps( self.cfg, sort_keys=True, indent=json_config.n_JSON_Indent ) + '\n'
        s_Out += str(self.args) + '\n'
        s_Out += "\n >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> SOLVER BACKEND:\n"
        s_Out += json.dumps( self.slvBE, sort_keys=True, indent=json_config.n_JSON_Indent ) + '\n'
        s_Out += "\n >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> CHECKER BACKENDS:\n"
        for chkBE in self.chkBEs:
            s_Out += json.dumps( chkBE, sort_keys=True, indent=json_config.n_JSON_Indent ) + '\n'
        return s_Out
                

##############################################################################################
################### The MZNTest class
##############################################################################################
class MznTest:

    ## Produce a pair: first, a string identifying the instance which is given as a string of the instance files
    ## Second, same without paths and file extensions for short printing
    ## the elements are sorted according to the extensions list, then alphabetically
    def getIName( self, s_Inst ):
        lId = s_Inst.split()     ## list of instance files
        lExt = self.params.cfg["COMMON_OPTIONS"]["Instance_List"]["InstanceFileExt"]
        lId = sorted( lId, key = lambda nm:
                ( lExt.index( os.path.splitext(nm)[1] ) \
                        if os.path.splitext(nm)[1] in lExt else len(lExt), os.path.basename( nm ) ) )
        lId2 = [ os.path.splitext( os.path.basename( fln ) )[0] for fln in lId ];
        return ' '.join(lId), ' '.join(lId2);

    def obtainParams( self ):
        self.params.obtainParams()

    ## If an instance was specified in cmdline, or model list(s) supplied
    def compileExplicitModelLists(self):
        ## Get cmdline filenames or from the --instList arguments
        ## Can compile the list from log files, see below
        self.params.instList = []
        ## Only if -l not used, take the pos args
        if (self.params.args.l_InstLists is None or 0==len( self.params.args.l_InstLists )) \
                and self.params.args.instanceFiles is not None and 0<len( self.params.args.instanceFiles ):
            self.params.instList.append( " ".join( self.params.args.instanceFiles ) )
        ## Mode "compare only" if (comparison lists and not run option) or no instances
        self.bCmpOnly = True if (not self.params.args.runAndCmp and (( \
                self.params.args.compare is not None and 0<len(self.params.args.compare)) or \
              ( self.params.args.l_InstLists is not None and 0<len( self.params.args.l_InstLists ) \
              and self.params.args.instanceFiles is not None and 0<len( self.params.args.instanceFiles ) ))) \
          else False
        ## If -l used, compile the inst list files
        if None!=self.params.args.l_InstLists and 0<len( self.params.args.l_InstLists ):
            for sFln in self.params.args.l_InstLists:
                with open( sFln, 'r' ) as rf:
                    for line in rf:
                        s_ModelFiles = ""
                        for wrd in line.split():
                            fIsMF = False
                            for ext in self.params.cfg["COMMON_OPTIONS"]["Instance_List"]["InstanceFileExt"]:
                                if wrd.endswith( ext ):
                                    fIsMF = True
                                    break
                            if fIsMF:
                                s_ModelFiles += ' ' + wrd
                        if 0<len( s_ModelFiles ):
                          self.params.instList.append( s_ModelFiles )
        
    ## Result logs can be used to extract the instance list, if wished
    def compileResultLogs(self):
        self.cmpRes = cmp_result_logs.CompareLogs()
        cmpFileList = []
        if None!=self.params.args.compare:     ### If -c used
            cmpFileList += self.params.args.compare
        ### If -l used, interpret pos arguments as comparison logs
        if None!=self.params.args.l_InstLists and 0<len( self.params.args.l_InstLists ):
            cmpFileList += self.params.args.instanceFiles
        for sFlnLog in cmpFileList:
            nEntries = 0
            logCurrent, lLogNames = self.cmpRes.addLog( sFlnLog )
            print( "Reading result log '", sFlnLog, "'...  ", sep='', end='', flush=True )
            with open( sFlnLog, 'r' ) as rf:
                while True:
                    chJ = json_log.readLogJSONChunk( rf )
                    if None==chJ:
                        break
                    try:
                        logCurrent[ self.getIName( chJ["Inst_Files"] )[0] ] = chJ
                        if "TEST_NAME" in chJ:
                            lLogNames[1] = chJ[ "TEST_NAME" ]
                            print( "  TEST NAME: '", chJ[ "TEST_NAME" ],
                                  "'... ", sep='', end='', flush=True )
                    except:
                        print( "\n   WARNING: unrecognized result chunk in file '", sFlnLog,
                              "' before position", rf.tell(), ", doesn't contain all keys")
                    else:
                        nEntries += 1
            print( len(logCurrent), "different instances among", nEntries, "recognized entries."  )
        
    def runTheInstances(self):
        logCurrent, lLogNames = self.cmpRes.addLog( self.params.args.result )
        if self.params.sThisName!=lLogNames[0]:
            lLogNames[1] = self.params.sThisName
        ## TRUNCATING files first, then "a" - better on Win??
        sNow = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        self.sStartTime = sNow
        self.fileSol00 = utils.openFile_autoDir( self.params.args.resultUnchk, "w" )
        self.fileSol = utils.openFile_autoDir(self.params.args.result.format(sNow + 
          ("" if self.params.args.name is None else "__"+utils.flnfy(self.params.args.name))), "w" )
        self.fileFailName = self.params.cfg["COMMON_OPTIONS"]["SOLUTION_CHECKING"]["s_FailedSaveFile"][0].format(sNow)
        self.fileFail = None      ## Not opening yet
        self.nCheckedInstances = 0
        self.nChecksFailed = 0
        self.cmpRes.initListComparison()
        for i_Inst in range( len(self.params.instList) ):
            s_Inst = self.params.instList[ i_Inst ]
            self.initInstance( i_Inst, s_Inst )
            self.solveOriginal( s_Inst )
            try:
                if self.ifShouldCheck():
                    self.checkOriginal( s_Inst )
                else:
                    print( "NO CHECK,  total check-failed instances:", self.nChecksFailed,
                           "from", self.nCheckedInstances )
            except:
                print( "  WARNING: failed to check instance solution. ", sys.exc_info() )
            ## Saving to the main log:
            self.saveSolution( self.fileSol )
            ## Ranking:
            sSet_Inst = self.getIName( self.result["Inst_Files"] )
            logCurrent[ sSet_Inst[0] ] = self.result
            try:
                self.cmpRes.compareInstance( sSet_Inst )
                if i_Inst < len(self.params.instList)-1:
                    print( "STATS:  ", end='' )
                    self.cmpRes.summarizeCurrent( lLogNames )
                    print( "" )
            except:
                print( "  WARNING: failed to compare/rank instance. ", sys.exc_info() )

    def compareLogs(self):
        self.cmpRes.initListComparison()
        theList = [ lfn for lfn in self.params.instList ]
        if 0==len( theList ):
            theList = self.cmpRes.getInstanceUnion()
        for sInst in theList:
            try:
                self.cmpRes.compareInstance( self.getIName( sInst ) )
            except:
                print( "  ------  WARNING: failed to compare/rank instance. ",  )
                traceback.print_exc()
        self.cmpRes.summarizeCmp()
        print( self.cmpRes.summarizeFinalHdr(), end='' )

    def summarize(self):
        try:
            ### Printing summary
            print ('')
            print ('='*50)
            print('            SUMMARY')
            print ('='*50)
            stats = self.cmpRes.summarize()
            print (stats)
        except Exception as e:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            print(exc_type, exc_obj, fname, exc_tb.tb_lineno)
        if not self.bCmpOnly:
            print( "Printing stats log to: ", end="" )
            sFlnSL = sFlnStatLog.format( utils.flnfy(
                " ".join(self.params.args.l_InstLists if self.params.args.l_InstLists is not None else []) ) )
            print( sFlnSL )
            with utils.openFile_autoDir( sFlnSL, "a" ) as wf:
                wf.write( "\nRUN  " + self.sStartTime + "--" + datetime.datetime.now().__str__() + ":  " )
                wf.write( sys.argv.__str__() )
                wf.write( "\n" )
                wf.write( stats )
            print( "\nResult logs saved to '",  self.params.args.result,
               "', with the unchecked log in '", self.params.args.resultUnchk, "'; failed solutions saved to '",
               self.params.cfg["COMMON_OPTIONS"]["SOLUTION_CHECKING"]["s_FailedSaveFile"][0],
               "' in an \"appendable JSON\" format, cf. https://github.com/pvorb/jsml."
               ## no more for shell: "\nSolver/checker stdout(err) outputs saved to '" + sDirResults + "/last_stdout(err)_...txt'."
               , sep='')

##############################################################################################
######################### MZNTest average-level #############################

    ## init
    def initInstance( self, i_Inst, s_Inst ):
        print( "\n--------------------- INSTANCE %d of %d:  '" % (i_Inst+1, len( self.params.instList )),
              s_Inst, "' ----------------------", sep='' )
        self.result = OrderedDict()
        if 0==i_Inst and None!=self.params.args.name:
            self.result["TEST_NAME"] = self.params.args.name
        self.result[ "Inst_Index_Of" ] = [ (i_Inst+1), len( self.params.instList ) ]
        self.result["Inst_Files"] = s_Inst
        self.solList = []                   ## The solution list
        ## TODO: instance name source?

    ## Solve the original instance
    def solveOriginal( self, s_Inst ):
        self.result["__SOLVE__"] = self.solveInstance( s_Inst, self.params.slvBE, '_SOLVING', self.solList )
        # print( "   RESULT:\n", self.result )
        self.saveSolution( self.fileSol00 )
        
    ## Solve a given MZN instance and return the result map
    ## Arguments: instance files in a string, backend parameter dictionary
    ## slvName is used for screen output and last_std(out/err)_... naming, so better no spaces
    ## solList provided <=> this is solving (not checking) and will use --checkDZN if opted

    ##  TODO
    ## Because we cannot limit memory of the subprocesses directly AND there seem to be bugs in the Python 3.4 impl,
    ## could replace the subprocess call by a call to an external which would run under given memory/time limits
    ## and save output to given files.
    ## OR: update from Python 3.4.2?
    ## NAming output / model files: sort input filenames, replace spaces/punctuation
    ### USE smth like
    ## keepcharacters = (' ','.','_')
    ## "".join(c for c in filename if c.isalnum() or c in keepcharacters else 'I').strip()
    ## PARAMETERS
    ##   : solList: if not None, we are solving originally ( not checking )
    def solveInstance(self, s_Inst, slvBE, slvName, solList=None):
        resSlv = OrderedDict()
        bChkDZN = True if None!=solList and None!=self.params.args.checkDZN else False
        if bChkDZN:
            print( "_PARSING '", self.params.args.checkDZN, sep='', end="'... " )
            with open( self.params.args.checkDZN, 'r' ) as ro:
                mzn_exec.parseStdout( ro, resSlv, slvBE["Stdout_Keylines"], slvBE["Stdout_Keyvalues"], solList )
                print( ro.tell(), "bytes", end='' )
            if None!=self.params.args.checkStderr:
                print( "  and '", self.params.args.checkStderr, sep='', end="'... " )
                with open( self.params.args.checkStderr, 'r' ) as re:
                    mzn_exec.parseStderr( re, resSlv, slvBE["Stderr_Keylines"], slvBE["Stderr_Keyvalues"] )
                    print( re.tell(), "bytes", end='' )
        else:      #### Solving oneself
            print( slvName, "... ", sep='', end='', flush=True )
            s_Call = slvBE["EXE"]["s_SolverCall"][0] % s_Inst \
              + ' ' + slvBE["EXE"]["s_ExtraCmdline"][0]
            if self.params.args.addOption is not None:
                for sOpt in self.params.args.addOption:
                    s_Call += ' ' + sOpt
            if solList is not None:
                if self.params.args.addSolverOption is not None:
                    for sOpt in self.params.args.addSolverOption:
                        s_Call += ' ' + sOpt
            else:
                if self.params.args.addCheckerOption is not None:
                    for sOpt in self.params.args.addCheckerOption:
                        s_Call += ' ' + sOpt
            s_InstMerged = s_Inst.strip()
            ## s_InstMerged = regex.sub( r"[.\\/:~]", "", s_InstMerged );
            ## s_InstMerged = regex.sub( r"[ ]", "-", s_InstMerged );
            keepcharacters = ('-','_')
            s_InstMerged = "".join(c if c.isalnum() or c in keepcharacters else 'I' for c in s_InstMerged).strip()
            
            if solList is not None and self.params.args.useJoinedName is not None:
                for sUseJN in self.params.args.useJoinedName:
                    s_UsingOpt = sUseJN % s_InstMerged
                    s_Call += ' ' + s_UsingOpt
            if solList is not None:      ## solving the original instance
                sFlnStdout = sFlnStdoutBase.format( s_InstMerged )
                sFlnStderr = sFlnStderrBase.format( s_InstMerged )
                if self.params.args.debug is not None and ( self.params.args.debug & 1 ):
                    print( "  CALL: \"", s_Call, "\"", sep='', flush=True )
            else:
                sFlnStdout = 'last_stdout' + slvName + '.txt'
                sFlnStderr = 'last_stderr' + slvName + '.txt'
                if self.params.args.debug is not None and ( self.params.args.debug & 2 ):
                    print( "  CALL: \"", s_Call, "\"", sep='', flush=True )
            resSlv["Solver_Call"] = s_Call
            resSlv["DateTime_Start"] = datetime.datetime.now().__str__()
            if 1==self.params.args.psutils:
                completed, tmAll = \
                    mzn_exec.runCmd(
                      s_Call,
                      slvBE["EXE"]["b_ThruShell"][0],
                      slvBE["EXE"]["n_TimeoutRealHard"][0],
                      slvBE["EXE"]["n_VMEMLIMIT_SoftHard"]
                    )
                with utils.openFile_autoDir( sFlnStdout, "w" ) as tf:
                    tf.write( completed.stdout )
                with utils.openFile_autoDir( sFlnStderr, "w" ) as tf:
                    tf.write( completed.stderr )
                print( "STDOUT/ERR: ", len(completed.stdout), '/',
                      len(completed.stderr), " bytes", sep='', end=', ' )
                mzn_exec.parseStderr( io.StringIO( completed.stderr ), resSlv, slvBE["Stderr_Keylines"], slvBE["Stderr_Keyvalues"] )
                mzn_exec.parseStdout( io.StringIO( completed.stdout ), resSlv, slvBE["Stdout_Keylines"], slvBE["Stdout_Keyvalues"], solList )
                ## Adding the outputs to the log
                ## resSlv["StdErr"] = completed.stderr
                ## resSlv["StdOut"] = completed.stdout
            else:          ## use the 'system' call
                with utils.openFile_autoDir( sFlnStdout, "w" ) as tf:
                    tf.write( "% EMPTY\n" )
                with utils.openFile_autoDir( sFlnStderr, "w" ) as tf:
                    tf.write( "% EMPTY" )
                tmAll = mzn_exec.runCmdCmdline(
                      s_Call,
                      sFlnStdout, sFlnStderr,
                      self.params.cfg["COMMON_OPTIONS"]["runCommand"],
                      slvBE["EXE"]["n_TimeoutRealHard"][0],
                      (self.params.args.verbose) if solList is not None else (self.params.args.vc),
                      slvBE["EXE"]["n_VMEMLIMIT_SoftHard"]
                )
                with open( sFlnStderr, "r" ) as rf:
                    mzn_exec.parseStderr( rf, resSlv, slvBE["Stderr_Keylines"], slvBE["Stderr_Keyvalues"] )
                with open( sFlnStdout, "r" ) as rf:
                    mzn_exec.parseStdout( rf, resSlv, slvBE["Stdout_Keylines"], slvBE["Stdout_Keyvalues"], solList )

            print( " t: {:.3f}".format( tmAll ), end=' s, ' )
            resSlv["DateTime_Finish"] = datetime.datetime.now().__str__()
            resSlv["TimeReal_All"] = tmAll
            resSlv["TimeReal_LastStatus"] = 0
            resSlv["Hostname"] = platform.uname()[1]
        ### For all cases, some postprocessing #############################
        if "Sol_Status" not in resSlv:
            resSlv["Sol_Status"] = [-50, "   !!!!!  STATUS TOTALLY UNKNOWN - NO STATUS LINE PARSED."]
        if "Time_Flt" not in resSlv or utils.try_float( resSlv.get( "Time_Flt" ) ) is None:
            resSlv["NOFZN"] = ["      !!!!! No flattening finish time registered or successfully parsed"]
        dTmLast = utils.try_float( resSlv.get( "RealTime_Solns2Out" ) )
        if None!=dTmLast:
            resSlv["TimeReal_LastStatus"] = dTmLast
            resSlv.pop( "RealTime_Solns2Out" )
        ## if "SolutionLast" in resSlv:
        ##     print( "   SOLUTION_LAST:\n", resSlv["SolutionLast"], sep='' )
        if None!=solList:
            print( "  Nsol:", len(solList), end=',' )
        print( "   STATUS:", resSlv["Sol_Status"] )
        return resSlv
      
    ## Append the unchecked solution of the instance to a temp. file
    def saveSolution(self, wf):
        json_log.writeLogChunk( wf, json.dumps( self.result, indent=json_config.n_JSON_Indent ) )
    
    ## Return the necessity to check solutions.
    ## Can be false if we only want to compare different solvers.
    def ifShouldCheck(self):
        return \
          0<self.params.cfg["COMMON_OPTIONS"]["SOLUTION_CHECKING"]["n_FailedSaveMax"][0] and \
          0!=self.params.cfg["COMMON_OPTIONS"]["SOLUTION_CHECKING"]["n_CheckedMax"][0] and \
          0<len( self.params.chkBEs ) and \
          self.ifCheckableStatus( self.result["__SOLVE__"]["Sol_Status"][0] )
    
    ##
    def ifCheckableStatus( self, status ):
        return status>0
    
    ## Check selected solutions of the instance
    def checkOriginal( self, s_Inst ):
        nCM = self.params.cfg["COMMON_OPTIONS"]["SOLUTION_CHECKING"]["n_CheckedMax"][0]
        nFSM = self.params.cfg["COMMON_OPTIONS"]["SOLUTION_CHECKING"]["n_FailedSaveMax"][0]
        assert 0!=nCM
        assert 0<len(self.solList)
        rng = None
        if 0<nCM:
            rng = range( 0, min( nCM, len(self.solList ) ) )
        else:
            rng = range( -1, max( nCM-1, -len(self.solList)-1 ), -1 )
        self.result["SOLUTION_CHECKS_DONE"] = 0
        self.result["SOLUTION_CHECKS_FAILED"] = 0
        fFailed = 0
        ## Iterate over the solution range
        for iSol in rng:
            ## Try modify
            # self.solList[ iSol ] = self.solList[iSol][:20] + '1' + self.solList[iSol][21:]
            print ( "CHK SOL ", iSol if iSol<0 else (iSol+1), '/', len(rng), sep='', end='... ' )
            with utils.openFile_autoDir( sFlnSolLastDzn, "w" ) as wf:          ## Save the selected solution
                wf.write( self.solList[ iSol ] )
            s_IC = s_Inst + ' ' + sFlnSolLastDzn
            bCheckOK = True
            chkFlSt = []
            # self.result["__CHECKS__"] = []
            for iChk in range ( len ( self.params.chkBEs ) ):
                chkBE = self.params.chkBEs[ iChk ]
                chkRes = self.solveInstance( s_IC, chkBE, '__Checker_'+str(iChk+1) )
                # self.result["__CHECKS__"].append( chkRes )
                if ( ## -51==chkRes["Sol_Status"][0] or               ## NOFZN? No, flattener should report INFEAS.
                    ( 0>chkRes["Sol_Status"][0] and -3<=chkRes["Sol_Status"][0] ) ):   ## INFEAS
                    bCheckOK = False
                    chkFlSt = chkRes["Sol_Status"]
            self.result["SOLUTION_CHECKS_DONE"] += 1
            if not bCheckOK:
                fFailed = 1
                self.result["SOLUTION_CHECKS_FAILED"] += 1
                self.result["SOLUTION_FAILED_LAST"] = self.solList[ iSol ]
                # self.result["SOLUTION_FAILED_LAST__CHKSTATUS"] = chkRes["Sol_Status"]
                if self.fileFail is None:
                    self.fileFail = utils.openFile_autoDir( self.fileFailName, "w" )
                self.saveSolution( self.fileFail )
                if nFSM<=self.result["SOLUTION_CHECKS_FAILED"]:
                    print ( self.result["SOLUTION_CHECKS_FAILED"], "failed solution(s) saved, go on" )
                    break
              
        self.nCheckedInstances += 1
        self.nChecksFailed += fFailed
        print( "   CHECK FAILS on this instance: ", self.result["SOLUTION_CHECKS_FAILED"],
                ",  total check-failed instances: ", self.nChecksFailed,
                " from ", self.nCheckedInstances, sep='' )
            
    def __init__(self):
        ## Default params
        self.params = MZT_Param()

##############################################################################################
######################### MZNTest public #############################
##############################################################################################
    def run(self):
        self.obtainParams()
        self.compileExplicitModelLists()
        self.compileResultLogs()
        if not self.bCmpOnly:
            self.runTheInstances()
        else:
            self.compareLogs()
        self.summarize()
        
#  def main
mznTst = MznTest()
mznTst.run()

