#!/usr/bin/env python
# coding: utf-8
# TST test
# (c) 2012-2016 Dalton Serey, UFCG

from __future__ import print_function
from __future__ import unicode_literals
from collections import Counter
from builtins import str
import os
import re
import sys
import json
import glob
import shlex
import signal
import string
import codecs
import unicodedata
import argparse
import difflib
import logging

from tst.jsonfile import JsonFile
import tst
from tst.utils import _assert, data2json
from tst.utils import cprint, to_unicode
from tst.colors import *

from subprocess import Popen, PIPE, check_output, CalledProcessError

TIMEOUT_DEFAULT = 2
PYTHON = 'python2.7'
if sys.version_info < (2,7):
    sys.stderr.write('tst.py: requires python 2.7 or later\n')
    sys.exit(1)

REDIRECTED = os.fstat(0) != os.fstat(1)

STATUS_CODE = {
    # TST test codes
    'DefaultError': 'e',
    'Timeout': 't',
    'Success': '.',
    'QuasiSuccess': '*',
    'AllTokensSequence': '@',
    'AllTokensMultiset': '&',
    'MissingTokens': '%',
    'Fail': 'F',
    'ScriptTestError': '!',
    'Inconclusive': '?',
    'UNKNOWN': '-',
    'NoInterpreterError': 'X',

    # Python ERROR codes
    'AttributeError': 'a',
    'SyntaxError': 's',
    'EOFError': 'o',
    'ZeroDivisionError': 'z',
    'IndentationError': 'i',
    'IndexError': 'x',
    'ValueError': 'v',
    'TypeError': 'y',
    'NameError': 'n',
}

TSTJSONFAILMSG = LRED + "Spec file is not a valid config object" + RESET

def alarm_handler(signum, frame):
    raise CutTimeOut

signal.signal(signal.SIGALRM, alarm_handler)


def unpack_results(run_method):

    def wrapper(self, *args, **kwargs):
        run_method(self, *args, **kwargs)

        if self.testcase.type == 'io':
            self.result['summary'] = STATUS_CODE[self.result['status']]

        return self.result

    return wrapper


def parse_test_report(data):
    if data and data[0] == '{':
        # assume it's json
        try:
            report = json.loads(data)
            if 'summary' not in report:
                return None, None
            return report['summary'], report.get('feedback')
        except:
            pass

    parts = data.split('\n', 2)
    # TODO: Spaces should be accepted as summary lines.
    #       Improve the specification of a valid summary line.
    if ' ' in parts[0]:
        return None, None

    summary = parts[0]
    if len(parts) > 2:
        feedback = parts[2]
    else:
        feedback = None

    return summary, feedback


class TestRun:

    def __init__(self, subject, testcase):
        self.subject = subject
        self.testcase = testcase
        self.result = {}
        self.result['type'] = self.testcase.type
        self.result['name'] = self.testcase.name

    def run(self, timeout=TIMEOUT_DEFAULT):
        if self.testcase.type == 'io':
            return self.run_iotest()

        elif self.testcase.type == 'script':
            return self.run_script()

        else:
            _assert(False, 'unknown test type')

    @unpack_results
    def run_script(self, timeout=TIMEOUT_DEFAULT):
        if "{}" in self.testcase.script:
            cmd_str = self.testcase.script.format(self.subject.filename)
        else:
            cmd_str = self.testcase.script
        cmd_str = re.sub(r'\bpython\b', 'python3', cmd_str)
        command = shlex.split(cmd_str)
        self.result['command'] = cmd_str

        process = Popen(command, stdout=PIPE, stderr=PIPE)
        signal.alarm(timeout)
        try:
            stdout, stderr = map(to_unicode, process.communicate())
            signal.alarm(0) # reset the alarm
            process.wait()
        except CutTimeOut: # program didn't stop within expected time
            process.terminate()
            self.result['status'] = 'Timeout'
            self.result['summary'] = 't'
            return self.result

        # collect test data
        self.result['exit_status'] = process.returncode
        self.result['stderr'] = stderr # comment out to remove from report
        self.result['stdout'] = stdout # comment out to remove from report

        # check for correct termination of the test script
        if self.result['exit_status'] != 0:
            self.result['status'] = 'ScriptTestError'
            self.result['summary'] = '!'
            return self.result

        # collect report from either stderr or stdout
        summary, feedback = parse_test_report(stderr)
        if not summary:
            summary, feedback = parse_test_report(stdout)

        if not summary:
            self.result['status'] = 'Inconclusive'
            self.result['summary'] = '?'
            return self.result

        self.result['summary'] = summary
        if summary == len(summary) * '.':
            self.result['status'] = 'Success'
        else:
            self.result['status'] = 'Fail'

        return self.result

    @unpack_results
    def run_iotest(self, timeout=TIMEOUT_DEFAULT):

        # define command
        config = tst.get_config()
        if config.get('run'):
            # use run option
            run = config['run']
            extensions = run.keys()
            ext = self.subject.filename.split('.')[-1]
            if ext not in extensions:
                self.result['status'] = 'NoInterpreterError'
                return self.result
            _assert(ext in extensions, "\nfatal: missing command for extension %s" % ext)
            command = config['run'][ext]
            cmd_str = "%s %s" % (command, self.subject.filename)
        else:
            # default is running through python
            cmd_str = '%s %s' % (PYTHON, self.subject.filename)

        command = shlex.split(cmd_str)

        # encode test input data
        if self.testcase.input:
            input_data = self.testcase.input.encode('utf-8')
        else:
            input_data = ''
        self.result['input'] = self.testcase.input
        self.result['output'] = self.testcase.output
        self.result['cases'] = self.testcase.cases
        self.result['sha1'] = self.testcase.sha1

        # loop until running the test
        while True:
            process = Popen(command, stdin=PIPE, stdout=PIPE, stderr=PIPE)
            signal.alarm(timeout)
            try:

                # run subject as external process
                process_data = process.communicate(input=input_data)
                stdout, stderr = map(to_unicode, process_data)

                # collect output data
                self.result['stdout'] = stdout # comment out to remove from report
                self.result['stderr'] = stderr # comment out to remove from report

                # reset alarm for future use
                signal.alarm(0)
                process.wait()

                # leave loop
                break

            except CutTimeOut:
                # timeout... give up
                process.terminate()
                self.result['status'] = 'Timeout'
                return self.result

            except OSError:
                # external error... must run again!
                process.terminate()

        # 1. Did an ERROR ocurred during execution?
        if process.returncode != 0:

            # set default status
            self.result['status'] = 'DefaultError'

            # try use error code from stderr, if available
            for error_code in STATUS_CODE:
                if error_code in stderr:
                    self.result['status'] = error_code
                    break

            return self.result

        # 2. Was it a PERFECTLY SUCCESSFUL run?
        preprocessed_stdout = preprocess(stdout, self.testcase.ignore)
        if self.testcase.preprocessed_output  == preprocessed_stdout:
            self.result['status'] = 'Success'
            return self.result

        # 3. Was it a NORMALIZED SUCCESSFUL run?
        normalized_stdout = preprocess(stdout, DEFAULT_OPS)
        if self.testcase.normalized_output == normalized_stdout:
            self.result['status'] = 'QuasiSuccess'
            return self.result

        # 4. Doesn't the testcase specify TOKENS?
        if not self.testcase.tokens:
            self.result['status'] = 'Fail'
            return self.result

        # set flags to use with methods re.*
        flags = re.DOTALL|re.UNICODE
        if 'case' in self.testcase.ignore:
            flags = flags|re.IGNORECASE

        # 5. Does the output have ALL EXPECTED TOKENS IN SEQUENCE?
        regex = '(.*)%s(.*)' % ('(.*)'.join(self.testcase.tokens))
        if re.match(regex, preprocessed_stdout, flags=flags):
            self.result['status'] = 'AllTokensSequence'
            return self.result

        # 6. Does the output have the TOKENS MULTISET?
        regex = '|'.join(self.testcase.tokens)
        found = re.findall(regex, preprocessed_stdout, flags=flags)
        found_ms = Counter(found)
        tokens_ms = Counter(self.testcase.tokens)
        if found_ms >= tokens_ms:
            self.result['status'] = 'AllTokensMultiset'
            return self.result

        # 7. Does the output have a proper SUBSET OF THE TOKENS?
        if found_ms <= tokens_ms and len(found_ms) > 0:
            self.result['status'] = 'MissingTokens'
            return self.result

        # 8. otherwise...
        self.result['status'] = 'Fail'
        return self.result


class TestSubject:

    def __init__(self, filename):
        self.filename = filename
        self._io_results = ''
        self.analyzer_results = ''
        self.testruns = []
        self._results = None
        self._summaries = None

    def results(self):
        if not self._results:
            self._results = []
            for tr in self.testruns:
                self._results.append(tr.result)

        return self._results

    def add_testrun(self, testrun):
        self.testruns.append(testrun)
        self._summaries = None

    def feedbacks(self):
        return [tr['feedback'] for tr in self.results() if tr.get('feedback')]

    def summaries(self, join_io=False):
        if not self._summaries:
            iosummaries = []
            self._summaries = []
            for tr in self.results():
                if join_io and tr['type'] == 'io':
                    iosummaries.append(tr['summary'])
                else:
                    self._summaries.append(tr['summary'])

            if iosummaries:
                self._summaries.insert(0, ''.join(iosummaries))

        return self._summaries

    def verdict(self):
        return 'success' if all(c == '.' for c in ''.join(self.summaries())) else 'fail'

    def summary(self):
        status_codes = [tr['summary'] for tr in self.results()]
        return ''.join(status_codes)


class TestCase():

    def __init__(self, test):

        # get data from tst.json
        self.name = test.get('name')
        self.input = test.get('input', '')
        self.output = test.get('output', '')
        self.cases = test.get('cases', None)
        self.sha1 = test.get('sha1', None)
        self.tokens = test.get('tokens', [])
        self.ignore = test.get('ignore', [])
        self.script = test.get('script')
        self.command = test.get('command')
        self.files = test.get('files')
        self.type = test.get('type') or ('script' if self.script or self.command else 'io')
        if self.type == 'script':
            _assert(self.command or self.script, "script tests must have a command")
            _assert(not self.command or not self.script, "script tests cannot have both script and command")
            _assert(not self.input and not self.output, "script tests cannot have input/output")
            _assert(not self.tokens, "script tests cannot have tokens")
            self.script = self.script or self.command

        # convert tokens to a list of strings, if necessary
        if isinstance(self.tokens, str):
            self.tokens = self.tokens.split()

        # convert ignore to a list of strings, if necessary
        if isinstance(self.ignore, str):
            self.ignore = self.ignore.split()

        # identify tokens within the expected output
        if not self.tokens and '{{' in self.output:
            p = r'{{(.*?)}}' # r'{{.*?}}|\[\[.*?\]\]'
            self.tokens = re.findall(p, self.output)
            # remove tokens' markup from expected output
            self.output = re.sub(p, lambda m: m.group(0)[2:-2], self.output)

        # preprocess individual tokens
        for i in range(len(self.tokens)):
            self.tokens[i] = preprocess(self.tokens[i], self.ignore)

        # set up preprocessed output
        self.preprocessed_output = preprocess(self.output, self.ignore)

        # set up normalized output
        self.normalized_output = preprocess(self.output, DEFAULT_OPS)

        # setup expression to match tokens (so we do it once for all subjects)
        self.tokens_expression = '|'.join(self.tokens) if self.tokens else ''


def preprocess(text, operator_names):

    # add whites if punctuation is used
    if 'punctuation' in operator_names and 'whites' not in operator_names:
        operator_names = operator_names + ['whites']

    # expand if all is requested
    if operator_names == ['all']:
        operator_names = OPERATOR.keys()

    _assert(all(name in OPERATOR for name in operator_names), "unknown operator in ignore")

    # sort to assure 'whites' is last
    operators = [OPERATOR[name] for name in sorted(operator_names)]

    # apply operators to text
    for op in operators:
        text = op(text)

    return text


def squeeze_whites(text):
    data = [lin.strip().split() for lin in text.splitlines()]
    data = [' '.join(line) for line in data]
    return '\n'.join(data)


def remove_linebreaks(text):
    # TODO: use carefully! it substitutes linebreaks for ' '
    return ' '.join(text.splitlines())


def drop_whites(text):
    # TODO: Use carefully! deletes all whites
    #       string.whitespace = '\t\n\x0b\x0c\r '
    table = dict((ord(char), None) for char in string.whitespace)
    return text.translate(table)


def punctuation_to_white(text):
    # WARNING: preprocess() silently adds 'whites' if punctuation is used
    # TODO: Use carefully! it substitutes punctuation for ' '
    # TODO: The specification is wrong!
    #       Punctuation should be changed to spaces? This will
    #       duplicate some whites... Should punctuation be deleted?
    #       This would merge tokens into a single one... Should we
    #       have a mixed behavior? All punctuation surrounded by white
    #       would be deleted and punctuation not surrounded
    # For now, it should be used with whites to work properly.
    table = dict((ord(char), u' ') for char in string.punctuation)
    return text.translate(table)


def strip_accents(text):
    # text must be a unicode object, not str
    try:
        nkfd_form = unicodedata.normalize('NFKD', text.decode('utf-8'))
        only_ascii = nkfd_form.encode('ASCII', 'ignore')
    except:
        nkfd_form = unicodedata.normalize('NFKD', text)
        only_ascii = nkfd_form.encode('ASCII', 'ignore')

    return only_ascii.decode('utf-8')


OPERATOR = {
    # 2to3: 'case': string.lower,
    'case': lambda c: c.lower(),
    'accents': strip_accents,
    'extra_whites': squeeze_whites,
    'linebreaks': remove_linebreaks, # not default
    'punctuation': punctuation_to_white, # not default
    'whites': drop_whites, # not default
}

DEFAULT_OPS = ['case', 'accents', 'extra_whites']

class CutTimeOut(Exception): pass

class StatusLine:

    def __init__(self):
        if not sys.stdout.isatty():
            self.terminal = False
            return

        self.terminal = True
        self.lastline = ''
        self.stty_size = os.popen('stty size', 'r').read()
        self.columns = int(self.stty_size.split()[1]) - 10

    def set(self, line):
        if not self.terminal:
            return
        GREEN = '\033[92m'
        RESET = '\033[0m'
        line = GREEN + line[:self.columns]+ RESET
        sys.stderr.write('\r%s\r' % ((1+len(self.lastline)) * ' '))
        sys.stderr.write(line)
        sys.stderr.flush()
        self.lastline = line

    def clear(self):
        if not self.terminal:
            return
        sys.stderr.write('\r%s\r' % ((1+len(self.lastline)) * ' '))
        sys.stderr.write('')
        sys.stderr.flush()
        self.lastline = ''


def indent(text):
    lines = text.splitlines()
    text = "\n".join(["    %s" % l for l in lines])
    return text


def color(color, text):
    reset = RESET
    if REDIRECTED:
        color = ""
        reset = ""
    return color + text + reset


class Reporter(object):
    VALID_REPORT_STYLES = ['summary', 'debug', 'failed', 'passed', 'worker']

    @staticmethod
    def get(style, num_tests=0, options={}):
        _assert(style is None or style in Reporter.VALID_REPORT_STYLES, 'invalid report style: {}'.format(style))
        if style is None or style == 'summary':
            return Reporter(options)

        elif style == 'debug':
            return DebugReporter(max_fails=1, options=options)

        elif style == 'failed':
            fail_filter = lambda s: s.count('.') != len(s)
            return FilterReporter(filtro=fail_filter, options=options)

        elif style == 'passed':
            passed_filter = lambda s: s.count('.') == len(s)
            return FilterReporter(filtro=passed_filter, options=options)

        elif style == 'worker':
            return WorkerReporter(options=options)

    def __init__(self, status_line=True, options={}):
        self.num_tests = 0
        self.tests_performed = 0
        self.current_subject = None
        self.testresults = None
        self.testcases = None
        self.subjects = []
        self.status_line = StatusLine() if status_line else None
        self.options = options

    def update(self, subject, testcase, testresult):
        if self.current_subject is None:
            hasattr(self, 'before_all') and self.before_all()

        if self.current_subject and subject != self.current_subject:
            # end current_subject report
            self.status_line and self.status_line.clear()
            self.print_report()
            hasattr(self, 'after_file') and self.after_file()
            hasattr(self, 'between_files') and self.between_files()

        if subject != self.current_subject:
            # change to new subject
            hasattr(self, 'before_file') and self.before_file()
            self.current_subject = subject
            self.testresults = []
            self.testcases = []
            self.subjects.append(subject)

        if subject:
            # update data
            self.testresults.append(testresult)
            self.testcases.append(testcase)
            self.tests_performed += 1
            self._status(self.tests_performed)

    def close(self):
        self.status_line and self.status_line.clear()
        self.print_report()
        hasattr(self, 'after_file') and self.after_file()
        hasattr(self, 'after_all') and self.after_all()

    def _status(self, tests_performed=None):
        if tests_performed is None:
            self.status_line.clear()
            return

        if not self.status_line: return
        self.status_line.set('%d of %d tests' % (tests_performed, self.num_tests))

    def _summaries(self):
        iosummaries = []
        _summaries = []
        for tr in self.testresults:
            if tr['type'] == 'io':
                iosummaries.append(tr['summary'])
            else:
                _summaries.append(tr['summary'])

        if iosummaries:
            _summaries.insert(0, ''.join(iosummaries))

        return ''.join(_summaries)

    def print_report(self):
        summary = self._summaries()
        line = '%s %s' % (summary, self.current_subject.filename)
        print(line, file=sys.stderr)


class FilterReporter(Reporter):
    def __init__(self, status_line=True, filtro=lambda s: True, options={}):
        super(FilterReporter, self).__init__(status_line, options)
        self.filtro = filtro

    def print_report(self):
        summary = self._summaries()
        if not self.filtro(summary): return
        self._filtered = True
        line = '%s %s' % (summary, self.current_subject.filename)
        print(line, file=sys.stderr)


class WorkerReporter(Reporter):
    def __init__(self, status_line=True, options={}):
        super(WorkerReporter, self).__init__(status_line, options)
        self._report = {}

    def print_report(self):
        self._report[self.current_subject.filename] = {
            "summary": "".join(self._summaries())
        }

    def after_all(self):
        print(data2json(self._report))


class DebugReporter(Reporter):
    def __init__(self, status_line=True, max_fails=1, options={}):
        super(DebugReporter, self).__init__(status_line, options)
        self.max_fails = max_fails

    def print_report(self):
        summary = self._summaries()
        line = '%s %s' % (summary, self.current_subject.filename)
        print(line, file=sys.stderr)
        if summary.count('.') == len(summary): return
        num_reported = 0
        for i in range(len(self.testcases)):
            if self.testresults[i]['summary'] == '.': continue
            testresult = self.testresults[i]
            if testresult['type'] == 'io':
                print(LGREEN + "input: " + RESET + repr(testresult['input']))
                print(LGREEN + "output: " + RESET + repr(testresult['output']))
                if self.options.get('diff'):
                    print(LGREEN + "diff:" + RESET)
                    print(indent(DebugReporter.external_diff(testresult)))
                if self.options.get('compare'):
                    print(LGREEN + "compare:" + RESET)
                    print(indent(DebugReporter.internal_diff(testresult)))
            elif testresult['type'] == 'script':
                cprint(LGREEN, 'script report:')
                print(indent(testresult['stderr']))
            else:
                logging.warning('unrecognized test type')
            num_reported += 1
            if num_reported == self.max_fails: break

    @staticmethod
    def external_diff(result):
        with codecs.open('oo', 'w', encoding='utf-8') as f:
            f.write(result['stdout'])
        with codecs.open('eo', 'w', encoding='utf-8') as f:
            f.write(result['output'])
        try:
            check_output(["diff", "oo", "eo"])
        except CalledProcessError as e:
            return e.output
        except OSError:
            logging.error("diff: cannot run external diff command")
        except:
            raise

    @staticmethod
    def internal_diff(result):
        differ = difflib.Differ()
        diff = differ.compare(result['stdout'].splitlines(), result['output'].splitlines())
        result = ''
        for e in diff:
            if e[0] == '+':
                line = color(LGREEN, "+ ") + color('\033[1;32;100m', e[2:])
            elif e[0] == '-':
                line = color(LRED, "- ") + color('\033[1;31;100m', e[2:])
            else:
                line = color('\033[2m', e)
            result += line + '\n'
        return result


def parse_cli():
    parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument('-a', '--activity', type=str, default=None, help='read activity specification from file ACTIVITY\n(default: tst.yaml and tst.json)')
    parser.add_argument('-t', '--test-files', type=str, default='*.yaml,*.json', help='read tests from TEST_FILES')
    parser.add_argument('-T', '--timeout', type=int, default=5, help='stop execution at TIMEOUT seconds')

    parser.add_argument('-d', '--diff', action="store_true", default=False, help='output failed testcases and expected output diff')
    parser.add_argument('-c', '--compare', action="store_true", default=False, help='output failed testcases and expected output color comparison')

    parser.add_argument('-f', '--format', type=str, help='output test results using FORMAT; alternatives\nare: debug (default) and worker')
    parser.add_argument('filename', nargs='*', default=[''])
    args = parser.parse_args()

    # identify answer files (files to be tested)
    if len(args.filename) == 1 and os.path.exists(args.filename[0]):
        files2test = [args.filename[0]]
    elif len(args.filename) == 1:
        fn_pattern = '*%s*' % args.filename[0]
        files2test = glob.glob(fn_pattern)
    else:
        files2test = args.filename

    _assert(not (args.diff or args.compare) or args.format in [None, 'debug'], 'diff and compare implies debug format')

    # identify test files (files containing tests)
    patterns2scan = args.test_files.split(",") if args.test_files else []
    test_files = []
    for pattern in patterns2scan:
        for filename in glob.glob(pattern):
            if filename not in test_files:
                test_files.append(filename)

    options = {
        "timeout": args.timeout,
        "output": args.format,
        "diff": args.diff,
        "compare": args.compare
    }
    return args.activity, files2test, test_files, options


def main():

    # parse command line
    activity, files2test, test_files, options = parse_cli()

    # read specification file
    tstjson = tst.read_specification(activity, verbose=True)

    # check for files required by specification
    for pattern in tstjson.get('require', []):
        _assert(glob.glob(pattern), "Missing required files for this test: %s" % pattern)

    # read tests
    tests = []
    for filename in test_files:
        cprint(LCYAN, "Reading %s" % filename, end='')
        try:
            testsfile = JsonFile(filename, array2map="tests")
            cprint(LGREEN, " (%s tests)" % len(testsfile.get("tests", [])))
            tests.extend(testsfile["tests"])
        except KeyError as e:
            #_msg = " %s✗%s (no tests found)" % (LRED, YELLOW)
            #cprint(YELLOW, _msg)
            pass
        except tst.jsonfile.CorruptedJsonFile as e:
            _msg = " %s(invalid tst file)" % LRED
            cprint(YELLOW, _msg)

    # make sure there are tests
    _assert(tests, '0 tests found')

    # filter files2test based on extensions and ignore_files
    config = tst.get_config()
    extensions = tstjson.get('extensions') or config['run'].keys() if 'run' in config else ['py']
    ignore_files = tstjson.get('ignore', []) + config.get('ignore', [])
    files2test = [f for f in files2test if any(f.endswith(e) for e in extensions) and f not in ignore_files]
    files2test.sort()
    _assert(files2test, 'No files to test')

    # read subjects and testcases
    subjects = [TestSubject(fn) for fn in files2test]
    testcases = [TestCase(t) for t in tests]

    # identify style
    if len(subjects) == 1 and options['output'] is None:
        style = 'debug'
    else:
        style = options['output'] or tst.get_config().get('report')

    reporter = Reporter.get(style=style, options=options)
    reporter.num_tests = len(subjects) * len(testcases)
    for subject in subjects:
        for testcase in testcases:
            testrun = TestRun(subject, testcase)
            testresult = testrun.run(timeout=options['timeout'])
            subject.add_testrun(testrun)
            reporter.update(subject, testcase, testresult)
    reporter.close()


class ColorizingStreamHandler(logging.StreamHandler):
    colors_map = {
        logging.DEBUG: LBLUE,
        logging.INFO: LCYAN,
        logging.WARNING: YELLOW,
        logging.ERROR: LRED,
        logging.CRITICAL: CRITICAL
    }

    def emit(self, record):
        try:
            message = self.format(record)
            self.stream.write(message + '\n')
            self.flush()
        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            self.handleError(record)

    def format(self, record):
        message = logging.StreamHandler.format(self, record)
        if getattr(self.stream, 'isatty', lambda: None)():
            parts = message.split('\n', 1)
            color = self.colors_map.get(record.levelno, '')
            parts[0] = color + parts[0] + RESET
            message = '\n'.join(parts)
        return message


if __name__ == '__main__':
    config = tst.get_config()

    log = logging.getLogger()
    log.setLevel(logging.DEBUG)

    handler_file = logging.FileHandler(os.path.expanduser('~/.tst/logs'))
    handler_file.setFormatter(logging.Formatter('%(asctime)s|%(name)s|%(levelname)s|%(message)s'))
    log.addHandler(handler_file)

    handler_console = ColorizingStreamHandler()
    log.addHandler(handler_console)

    if len(sys.argv) > 1 and sys.argv[1] == '--one-line-help':
        print('run tests specified in tst.json')
        sys.exit(0)

    try:
        main()
    except (KeyboardInterrupt, SystemExit):
        pass
    except Exception:
        log.critical('sorry... critical error')
        log.removeHandler(handler_console)
        log.exception('oops... an error occurred')
        sys.exit(1)
