#! /usr/bin/env python

"""
Usage::

    testsuite.py [OPTIONS]

Run the Langkit testsuite.
"""

import glob
import os
import shutil
import subprocess
import sys

from e3.testsuite import Testsuite

import drivers.adalog_driver
import drivers.langkit_support_driver
import drivers.lkt_build_and_run_driver
import drivers.lkt_compile_driver
import drivers.lkt_parse_driver
import drivers.lkt_toolbox_driver
import drivers.lkt_unparse_driver
import drivers.python_driver


def add_to_path(env_var, directory):
    """
    Add the given directory to the `env_var` path.
    """
    path = os.environ.get(env_var)
    path = (
        "{}{}{}".format(directory, os.path.pathsep, path) if path else env_var
    )
    os.environ[env_var] = path


class LangkitTestsuite(Testsuite):
    tests_subdir = "tests"
    test_driver_map = {
        "adalog": drivers.adalog_driver.AdalogDriver,
        "langkit_support": drivers.langkit_support_driver.LangkitSupportDriver,
        "lkt": drivers.lkt_toolbox_driver.LktToolboxDriver,
        "lkt_build_and_run": (
            drivers.lkt_build_and_run_driver.LktBuildAndRunDriver
        ),
        "lkt_compile": drivers.lkt_compile_driver.LktCompileDriver,
        "lkt_parse": drivers.lkt_parse_driver.LktParseDriver,
        "lkt_unparse": drivers.lkt_unparse_driver.LktUnparseDriver,
        "python": drivers.python_driver.PythonDriver,
    }

    def add_options(self, parser):
        parser.add_argument(
            "--with-python",
            help="If provided, use as the Python interpreter in testcases.",
        )

        parser.add_argument(
            "--coverage",
            "-C",
            action="store_true",
            help="Enable computation of code coverage for Langkit and"
            " Langkit_Support. This requires coverage.py and"
            " GNATcoverage.",
        )

        parser.add_argument(
            "--valgrind",
            action="store_true",
            help="Run tests with Valgrind to check memory issues.",
        )

        parser.add_argument(
            "--no-auto-path",
            action="store_true",
            help="Do not automatically add Langkit to PYTHONPATH. This is"
            " useful to test that Langkit was properly installed in the"
            " environment that runs the testsuite.",
        )

        parser.add_argument(
            "--disable-ocaml",
            action="store_true",
            help="Disable testcases that require OCaml (they are enabled by"
            " default).",
        )

        parser.add_argument(
            "--disable-java",
            action="store_true",
            help="Disable testcases that require Java (they are enabled by"
            " default).",
        )
        parser.add_argument(
            "--disable-native-image",
            action="store_true",
            help="Disable testcases that require native-image (they are"
            " enabled by default).",
        )
        parser.add_argument(
            "--maven-local-repo",
            help="Specify the Maven repository to use, default one is the"
            " user's repository (~/.m2).",
        )
        parser.add_argument(
            "--maven-executable",
            help="Specify the Maven executable to use. The default one is"
            ' "mvn".',
        )

        parser.add_argument(
            "--disable-gdb",
            action="store_true",
            help="Disable testcases that require GDB (they are enabled by"
            " default).",
        )

        parser.add_argument(
            "--restricted-env",
            action="store_true",
            help="Skip testcases that cannot run in a restricted environment"
            " (need for non-standard Python packages).",
        )

        #
        # Convenience options for developpers
        #

        parser.add_argument(
            "--disable-tear-up-builds",
            "-B",
            action="store_true",
            help="Disable the automatic build of Langkit_Support during the"
            " testsuite tear_up step. This is used to speed up successive"
            " testsuite runs during development.",
        )
        parser.add_argument(
            "--pretty-print",
            action="store_true",
            help="Pretty-print generated source code.",
        )

        # Tests update
        parser.add_argument(
            "--rewrite",
            "-r",
            action="store_true",
            help="Rewrite test baselines according to current output.",
        )

    def set_up(self):
        super().set_up()

        args = self.main.args

        self.env.rewrite_baselines = args.rewrite
        self.env.control_condition_env = {
            "restricted_env": args.restricted_env,
            "has_ocaml": not args.disable_ocaml,
            "has_java": not args.disable_java,
            "has_native_image": not args.disable_native_image,
            "has_gdb": not args.disable_gdb,
            "os": self.env.build.os.name,
            "valgrind": args.valgrind,
        }

        if args.coverage:
            # Create a directory that we'll use to:
            #
            #   1) collect coverage data for each testcase;
            #   2) generate the HTML report.
            self.env.coverage_dir = os.path.join(self.output_dir, "coverage")
            self.env.langkit_support_coverage_dir = os.path.join(
                self.env.coverage_dir, "langkit_support"
            )
            os.mkdir(self.env.coverage_dir)
            os.mkdir(self.env.langkit_support_coverage_dir)

        def run(name, args):
            p = subprocess.run(
                args,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                encoding="utf-8",
            )
            if p.returncode != 0:
                raise RuntimeError(
                    "{} build failed (GPRbuild returned {})\n{}".format(
                        name, p.returncode, p.stdout
                    )
                )

        # Build Langkit_Support so that each testcase does not try to build it
        # in parallel. To achieve this, we build+install it with all library
        # kinds, and then update the environment so that testcases can assume
        # it is installed.
        if not args.disable_tear_up_builds:
            langkit_root_dir = os.path.join(self.root_dir, "..")
            self.env.langkit_support_project_file = os.path.join(
                langkit_root_dir, "langkit", "support", "langkit_support.gpr"
            )
            install_prefix = os.path.join(self.working_dir, "install")
            if os.path.exists(install_prefix):
                shutil.rmtree(install_prefix)

            gargs = ["-p", "-P", self.env.langkit_support_project_file]
            cargs = ["-cargs", "-O0", "-g", "-gnatwae"]
            if args.coverage:
                gargs.append("--subdirs=gnatcov")
                cargs.extend(["-fdump-scos", "-fpreserve-control-flow"])

            for build in ("static", "relocatable"):
                run(
                    f"Langkit support - build {build}",
                    ["gprbuild"]
                    + gargs
                    + ["-XLIBRARY_TYPE={}".format(build)]
                    + cargs,
                )

                run(
                    f"Langkit support - install {build}",
                    [
                        "gprinstall",
                        "-P",
                        self.env.langkit_support_project_file,
                        "-p",
                        "--sources-subdir=include/langkit_support",
                        "-XLIBRARY_TYPE={}".format(build),
                        "--prefix={}".format(install_prefix),
                        "--build-var=LIBRARY_TYPE",
                        "--build-name={}".format(build),
                    ],
                )

            # Make the installed library available to all testcases
            add_to_path("PATH", os.path.join(install_prefix, "bin"))
            add_to_path("LD_LIBRARY_PATH", os.path.join(install_prefix, "lib"))
            add_to_path(
                "GPR_PROJECT_PATH",
                os.path.join(install_prefix, "share", "gpr"),
            )

            # On GNU/Linux, the OCaml and Java bindings need to have access
            # to the sigsegv_handler shared object. Build it and add it to
            # the environment.
            if (
                not args.disable_ocaml or not args.disable_java
            ) and self.env.build.os.name == "linux":
                sigsegv_handler_dir = os.path.join(
                    langkit_root_dir, "sigsegv_handler"
                )
                sigsegv_handler_prj = os.path.join(
                    sigsegv_handler_dir, "langkit_sigsegv_handler.gpr"
                )
                run(
                    "Langkit_SIGSEGV_Handler",
                    ["gprbuild", "-p", "-P", sigsegv_handler_prj],
                )
                add_to_path(
                    "LD_LIBRARY_PATH", os.path.join(sigsegv_handler_dir, "lib")
                )

        # If Java is enabled and there is a Maven local repo, export it
        if not args.disable_java:
            if args.maven_executable:
                os.environ["MAVEN_EXECUTABLE"] = args.maven_executable
            if args.maven_local_repo:
                os.environ["MAVEN_LOCAL_REPO"] = args.maven_local_repo

    def adjust_dag_dependencies(self, dag):
        # Crude assumption that should do the work.
        #
        # If there are more tests than testsuite jobs, assume running tests in
        # parallel will keep all cores busy most of the time, so disable
        # parallelism inside each tests. This is necessary to avoid exceeding
        # our job limit in full testsuite runs.
        #
        # Otherwise, assume we are running only a handful of tests, in which
        # case we want to run them as fast as possible (for dev convenience):
        # forward testsuite parallelism inside each test.
        self.env.inner_jobs = (
            1
            if len(dag.vertex_data) >= self.main.args.jobs
            else self.main.args.jobs
        )

    def tear_down(self):
        if self.main.args.coverage:
            # Consolidate coverage data for each testcase and generate both a
            # sumary textual report on the standard output and a detailed HTML
            # report.
            langkit_cov_files = glob.glob(
                os.path.join(self.env.coverage_dir, "*.coverage")
            )

            lksp_cov_files = glob.glob(
                os.path.join(self.env.coverage_dir, "*.trace")
            )

            if langkit_cov_files:
                rcfile = os.path.join(self.root_dir, "..", "coverage.ini")
                for argv in (
                    ["combine"] + langkit_cov_files,
                    ["report"],
                    ["html", "-d.", "--title=Langkit coverage report"],
                ):
                    if argv[0] != "combine":
                        argv.append("--rcfile=" + rcfile)
                    subprocess.check_call(
                        ["coverage"] + argv, cwd=self.env.coverage_dir
                    )

                html_index = os.path.join(self.env.coverage_dir, "index.html")
                assert os.path.exists(html_index)
                print(
                    "Detailed HTML coverage report for Langkit available at:"
                    " {}".format(html_index)
                )
            else:
                print(
                    "No test exercised Langkit: no coverage report to"
                    " produce"
                )

            if lksp_cov_files:
                subprocess.check_call(
                    [
                        "gnatcov",
                        "coverage",
                        "-P",
                        self.env.langkit_support_project_file,
                        "--level=stmt+decision",
                        "--annotate=dhtml",
                        "--output-dir",
                        self.env.langkit_support_coverage_dir,
                        "--subdirs=gnatcov",
                    ]
                    + lksp_cov_files
                )
                print(
                    "HTML coverage report for Langkit_Support: {}".format(
                        os.path.join(
                            self.env.langkit_support_coverage_dir, "index.html"
                        )
                    )
                )
            else:
                print(
                    "No test exercised Langkit_Support: no coverage report"
                    " to produce"
                )

        super().tear_down()


if __name__ == "__main__":
    sys.exit(LangkitTestsuite().testsuite_main())
