#!/usr/bin/env python
"""
Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
See https://llvm.org/LICENSE.txt for license information.
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

Provides classes used by the test results reporting infrastructure
within the LLDB test suite.


Tests the process_control module.
"""


# System imports.
import os
import os.path
import unittest
import sys
import threading

# Our imports.
from test_runner import process_control


class TestInferiorDriver(process_control.ProcessDriver):
    def __init__(self, soft_terminate_timeout=None):
        super(TestInferiorDriver, self).__init__(
            soft_terminate_timeout=soft_terminate_timeout
        )
        self.started_event = threading.Event()
        self.started_event.clear()

        self.completed_event = threading.Event()
        self.completed_event.clear()

        self.was_timeout = False
        self.returncode = None
        self.output = None

    def write(self, content):
        # We'll swallow this to keep tests non-noisy.
        # Uncomment the following line if you want to see it.
        # sys.stdout.write(content)
        pass

    def on_process_started(self):
        self.started_event.set()

    def on_process_exited(self, command, output, was_timeout, exit_status):
        self.returncode = exit_status
        self.was_timeout = was_timeout
        self.output = output
        self.returncode = exit_status
        self.completed_event.set()


class ProcessControlTests(unittest.TestCase):
    @classmethod
    def _suppress_soft_terminate(cls, command):
        # Do the right thing for your platform here.
        # Right now only POSIX-y systems are reporting
        # soft terminate support, so this is set up for
        # those.
        helper = process_control.ProcessHelper.process_helper()
        signals = helper.soft_terminate_signals()
        if signals is not None:
            for signum in helper.soft_terminate_signals():
                command.extend(["--ignore-signal", str(signum)])

    @classmethod
    def inferior_command(cls, ignore_soft_terminate=False, options=None):
        # Base command.
        script_name = "{}/inferior.py".format(os.path.dirname(__file__))
        if not os.path.exists(script_name):
            raise Exception(
                "test inferior python script not found: {}".format(script_name)
            )
        command = [sys.executable, script_name]

        if ignore_soft_terminate:
            cls._suppress_soft_terminate(command)

        # Handle options as string or list.
        if isinstance(options, str):
            command.extend(options.split())
        elif isinstance(options, list):
            command.extend(options)

        # Return full command.
        return command


class ProcessControlNoTimeoutTests(ProcessControlTests):
    """Tests the process_control module."""

    def test_run_completes(self):
        """Test that running completes and gets expected stdout/stderr."""
        driver = TestInferiorDriver()
        driver.run_command(self.inferior_command())
        self.assertTrue(driver.completed_event.wait(5), "process failed to complete")
        self.assertEqual(driver.returncode, 0, "return code does not match")

    def test_run_completes_with_code(self):
        """Test that running completes and gets expected stdout/stderr."""
        driver = TestInferiorDriver()
        driver.run_command(self.inferior_command(options="-r10"))
        self.assertTrue(driver.completed_event.wait(5), "process failed to complete")
        self.assertEqual(driver.returncode, 10, "return code does not match")


class ProcessControlTimeoutTests(ProcessControlTests):
    def test_run_completes(self):
        """Test that running completes and gets expected return code."""
        driver = TestInferiorDriver()
        timeout_seconds = 5
        driver.run_command_with_timeout(
            self.inferior_command(), "{}s".format(timeout_seconds), False
        )
        self.assertTrue(
            driver.completed_event.wait(2 * timeout_seconds),
            "process failed to complete",
        )
        self.assertEqual(driver.returncode, 0)

    def _soft_terminate_works(self, with_core):
        # Skip this test if the platform doesn't support soft ti
        helper = process_control.ProcessHelper.process_helper()
        if not helper.supports_soft_terminate():
            self.skipTest("soft terminate not supported by platform")

        driver = TestInferiorDriver()
        timeout_seconds = 5

        driver.run_command_with_timeout(
            # Sleep twice as long as the timeout interval.  This
            # should force a timeout.
            self.inferior_command(options="--sleep {}".format(timeout_seconds * 2)),
            "{}s".format(timeout_seconds),
            with_core,
        )

        # We should complete, albeit with a timeout.
        self.assertTrue(
            driver.completed_event.wait(2 * timeout_seconds),
            "process failed to complete",
        )

        # Ensure we received a timeout.
        self.assertTrue(driver.was_timeout, "expected to end with a timeout")

        self.assertTrue(
            helper.was_soft_terminate(driver.returncode, with_core),
            (
                "timeout didn't return expected returncode "
                "for soft terminate with core: {}"
            ).format(driver.returncode),
        )

    def test_soft_terminate_works_core(self):
        """Driver uses soft terminate (with core request) when process times out."""
        self._soft_terminate_works(True)

    def test_soft_terminate_works_no_core(self):
        """Driver uses soft terminate (no core request) when process times out."""
        self._soft_terminate_works(False)

    def test_hard_terminate_works(self):
        """Driver falls back to hard terminate when soft terminate is ignored."""

        driver = TestInferiorDriver(soft_terminate_timeout=2.0)
        timeout_seconds = 1

        driver.run_command_with_timeout(
            # Sleep much longer than the timeout interval,forcing a
            # timeout.  Do whatever is needed to have the inferior
            # ignore soft terminate calls.
            self.inferior_command(ignore_soft_terminate=True, options="--never-return"),
            "{}s".format(timeout_seconds),
            True,
        )

        # We should complete, albeit with a timeout.
        self.assertTrue(driver.completed_event.wait(60), "process failed to complete")

        # Ensure we received a timeout.
        self.assertTrue(driver.was_timeout, "expected to end with a timeout")

        helper = process_control.ProcessHelper.process_helper()
        self.assertTrue(
            helper.was_hard_terminate(driver.returncode),
            (
                "timeout didn't return expected returncode "
                "for hard teriminate: {} ({})"
            ).format(driver.returncode, driver.output),
        )

    def test_inferior_exits_with_live_child_shared_handles(self):
        """inferior exit detected when inferior children are live with shared
        stdout/stderr handles.
        """
        # Requires review D13362 or equivalent to be implemented.
        self.skipTest("http://reviews.llvm.org/D13362")

        driver = TestInferiorDriver()

        # Create the inferior (I1), and instruct it to create a child (C1)
        # that shares the stdout/stderr handles with the inferior.
        # C1 will then loop forever.
        driver.run_command_with_timeout(
            self.inferior_command(
                options="--launch-child-share-handles --return-code 3"
            ),
            "5s",
            False,
        )

        # We should complete without a timetout.  I1 should end
        # immediately after launching C1.
        self.assertTrue(driver.completed_event.wait(5), "process failed to complete")

        # Ensure we didn't receive a timeout.
        self.assertFalse(driver.was_timeout, "inferior should have completed normally")

        self.assertEqual(
            driver.returncode,
            3,
            "expected inferior process to end with expected returncode",
        )


if __name__ == "__main__":
    unittest.main()
