#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (C) 2018  Nexedi SA and Contributors.
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
"""runTestSuite - run neotest under Nexedi testing infrastructure.

neotest must be on $PATH.
"""

from erp5.util.taskdistribution import TaskDistributor
from subprocess import Popen, PIPE
from time import time, strftime, gmtime
import os, sys, threading, argparse, logging, traceback, re


def main():
    # testnode executes us giving URL to master results collecting instance and other details
    # https://lab.nexedi.com/nexedi/erp5/blob/744f3fde/erp5/util/testnode/UnitTestRunner.py#L137
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--master_url', help='The URL of Master controling many suites')
    parser.add_argument('--revision', help='The revision to test', default='dummy_revision')
    parser.add_argument('--test_suite', help='The test suite name')
    parser.add_argument('--test_suite_title', help='The test suite title')
    parser.add_argument('--test_node_title', help='The test node title')
    parser.add_argument('--project_title', help='The project title')

    parser.add_argument('--verbose', action='store_true', help='increase output verbosity')

    args = parser.parse_args()

    # if verbose -> log to stderr
    logger = None
    if args.verbose:
        logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
        logger = logging.getLogger()

    # connect to master and create 'test result' object with list of tests to run
    tool = TaskDistributor(portal_url = args.master_url, logger = logger)
    test_result = tool.createTestResult(
                    revision        = args.revision,
                    test_name_list  = ['test-go', 'test-py', 'bench-local'],
                    node_title      = args.test_node_title,
                    test_title      = args.test_suite_title or args.test_suite,
                    project_title   = args.project_title)

    if test_result is None:
        # a test run for given name and revision has already been completed
        return

    # make sure we get output from subprocesses without delay.
    # go does not buffer stdout/stderr by default, but python does for stdout.
    # tell python not to buffer anything.
    os.environ['PYTHONUNBUFFERED'] = 'y'

    # run the tests
    devnull = open(os.devnull)
    while 1:
        # ask master for next test to run; stop if no more.
        test_result_line = test_result.start()
        if test_result_line is None:
            break

        # run `neotest <test-name>`
        testname = test_result_line.name
        argv = ['neotest', testname]
        tstart = time()

        try:
            # NOTE runs with unchanged cwd. Instance wrapper cares to set cwd before running us.
            # bufsize=1 means 'line buffered'
            p = Popen(argv, stdin=devnull, stdout=PIPE, stderr=PIPE, bufsize=1)
        except:
            stdout, stderr = '', traceback.format_exc()
            sys.stderr.write(stderr)
            ok = False
        else:
            # tee >stdout,stderr so we can also see in testnode logs
            # (explicit teeing instead of p.communicate() to be able to see incremental progress)
            buf_out = []
            buf_err = []
            tout = threading.Thread(target=tee, args=(p.stdout, sys.stdout, buf_out))
            terr = threading.Thread(target=tee, args=(p.stderr, sys.stderr, buf_err))
            tout.start()
            terr.start()

            tout.join(); stdout = ''.join(buf_out)
            terr.join(); stderr = ''.join(buf_err)
            p.wait()
            ok = (p.returncode == 0)

            # default status dict just by exit code
            status = {
                'test_count':       1,
                'error_count':      (0 if ok else 1),
                'failure_count':    0,
                'skip_count':       0,
                #html_test_result
            }

            # postprocess output, if we can
            summaryf = globals().get(testname.replace('-', '_') + '_summary')
            if summaryf is not None:
                try:
                    summary = summaryf(stdout)
                except:
                    bad = traceback.format_exc()
                    sys.stderr.write(bad)
                    stderr += bad
                    status['error_count'] += 1

                else:
                    status.update(summary)


        tend = time()

        # report result of test run back to master
        test_result_line.stop(
            command     = ' '.join(argv),
            duration    = tend - tstart,
            date        = strftime("%Y/%m/%d %H:%M:%S", gmtime(tend)),

            stdout      = stdout,
            stderr      = stderr,

            **status
        )

# tee, similar to tee(1) utility, copies data from fin to fout appending them to buf.
def tee(fin, fout, buf):
    while 1:
        # NOTE use raw os.read because it does not wait for full data to be available.
        # ( we could use fin.readline(), but there are cases when e.g. progress
        #   is reported via printing consequent dots on the same line and
        #   readline() won't work for that.
        #
        #   besides when a lot of output is available it would be a waste to
        #   read/flush it line-by-line. )
        data = os.read(fin.fileno(), 4096)
        if not(data):
            return  # EOF

        fout.write(data)
        fout.flush()
        buf.append(data)


# xint converts number from neo/py test output to integer
def xint(s):
    s = s.strip()
    if s == '.':
        return 0
    else:
        return int(s)


# extract summary from neo/py test run
def test_py_summary(stdout):
    #   Test Module |  run  | unexpected | expected | skipped |  time
    #   ...
    #       Summary |  366  |       .    |      9   |     .   | 353.47s
    m = re.search(r'^\s*summary.*$', stdout, re.M | re.I)
    assert m is not None, "could not find summary line"
    summary = m.group(0)
    _, nrun, nfail, nxfail, nskip, _  = summary.split('|')
    return {
            'test_count':       xint(nrun),
            'error_count':      xint(nfail),
            'failure_count':    xint(nxfail),
            'skip_count':       xint(nskip),
    }


if __name__ == '__main__':
    main()
