#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2019-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later

"""
Run interaction tests using event simulation.

Example usage from Blender's source dir:

This uses ``test_undo.py``, running the ``text_editor_simple`` function.

To run all tests:

   ./tests/python/ui_simulate/run.py --blender=blender.bin --tests '*'

For an editor to follow the tests:

   ./tests/python/ui_simulate/run.py --blender=blender.bin --tests '*' \
       --step-command-pre='gvim --remote-silent +{line} "{file}"'

"""

import os
import sys


def create_parser():
    import argparse
    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.RawTextHelpFormatter,
    )
    parser.add_argument(
        "--blender",
        dest="blender",
        required=True,
        metavar="BLENDER_COMMAND",
        help="Location of the blender command to run (when quoted, may include arguments).",
    )

    parser.add_argument(
        "--tests",
        dest="tests",
        nargs='+',
        required=True,
        metavar="TEST_ID",
        help="Names of tests to run, use '*' to run all tests.",
    )

    parser.add_argument(
        "--jobs", "-j",
        dest="jobs",
        default=1,
        type=int,
        help="Number of tests (and instances of Blender) to run in parallel.",
    )

    parser.add_argument(
        "--keep-open",
        dest="keep_open",
        default=False,
        action='store_true',
        required=False,
        help="Keep the Blender window open after running the test.",
    )

    parser.add_argument(
        "--list-tests",
        dest="list_tests",
        default=False,
        action='store_true',
        required=False,
        help="Show a list of available TEST_ID.",
    )

    parser.add_argument(
        "--step-command-pre",
        dest="step_command_pre",
        required=False,
        metavar="STEP_COMMAND_PRE",
        help=(
            "Command to run that takes the test file and line as arguments. "
            "Literals {file} and {line} will be replaced with the file and line."
            "Called for every event."
            "Called for every event, allows an editor to track which commands run."
        )
    )
    parser.add_argument(
        "--step-command-post",
        dest="step_command_post",
        required=False,
        metavar="STEP_COMMAND_POST",
        help=(
            "Command to run that takes the test file and line as arguments. "
            "Literals {file} and {line} will be replaced with the file and line."
            "Called for every event, allows an editor to track which commands run."
        )
    )

    return parser


def all_test_ids(directory):
    from types import FunctionType
    for f in sorted(os.listdir(directory)):
        if f.startswith("test_") and f.endswith(".py"):
            mod = __import__(f[:-3])
            for k, v in sorted(vars(mod).items()):
                if not k.startswith("_") and isinstance(v, FunctionType):
                    yield f.rpartition(".")[0] + "." + k


def list_tests(directory):
    for test_id in all_test_ids(directory):
        print(test_id)
    sys.exit(0)


def _process_test_id_fn(env, args, test_id):
    import subprocess
    import shlex

    directory = os.path.dirname(__file__)
    cmd = (
        *shlex.split(args.blender),
        "--enable-event-simulate",
        "--factory-startup",
        "--python", os.path.join(directory, "run_blender_setup.py"),
        "--",
        "--tests", test_id,
        *(("--keep-open",) if args.keep_open else ()),
        *(("--step-command-pre", args.step_command_pre) if args.step_command_pre else ()),
        *(("--step-command-post", args.step_command_post) if args.step_command_post else ()),
    )
    callproc = subprocess.run(cmd, env=env)
    return test_id, callproc.returncode == 0


def main():
    directory = os.path.dirname(__file__)
    if "--list-tests" in sys.argv:
        list_tests(directory)
        sys.exit(0)

    if "bpy" in sys.modules:
        raise Exception("Cannot run inside Blender")

    parser = create_parser()
    args = parser.parse_args()

    tests = args.tests

    # Validate tests exist
    test_ids = list(all_test_ids(directory))
    if tests[0] == "*":
        tests = test_ids
    else:
        for test_id in tests:
            if test_id not in test_ids:
                print(test_id, "not found in", test_ids)
                return

    env = os.environ.copy()
    env.update({
        "LSAN_OPTIONS": "exitcode=0",
    })

    # We could support multiple tests per Blender session.
    results = []
    results_fail = 0
    if args.jobs <= 1:
        for test_id in tests:
            _, success = _process_test_id_fn(env, args, test_id)
            results.append((test_id, success))
            if not success:
                results_fail += 1
    else:
        from concurrent.futures import ProcessPoolExecutor
        executor = ProcessPoolExecutor(max_workers=args.jobs)
        num_tests = len(tests)
        for test_id, success in executor.map(_process_test_id_fn, (env,) * num_tests, (args,) * num_tests, tests):
            results.append((test_id, success))
            if not success:
                results_fail += 1

    print(len(results), "tests,", results_fail, "failed")
    for test_id, ok in results:
        print("OK:  " if ok else "FAIL:", test_id)


if __name__ == "__main__":
    main()
