import multiprocessing as mp
import os

from numba import cuda
from numba.cuda.cudadrv.driver import CudaAPIError, driver
from numba.cuda.cudadrv.error import CudaSupportError
from numba.cuda.testing import skip_on_cudasim, unittest, CUDATestCase


# A mock of cuInit that always raises a CudaAPIError
def cuInit_raising(arg):
    raise CudaAPIError(999, "CUDA_ERROR_UNKNOWN")


# Test code to run in a child that patches driver.cuInit to a variant that
# always raises. We can't use mock.patch.object here because driver.cuInit is
# not assigned until we attempt to initialize - mock.patch.object cannot locate
# the non-existent original method, and so fails. Instead we patch
# driver.cuInit with our raising version prior to any attempt to initialize.
def cuInit_raising_test(result_queue):
    driver.cuInit = cuInit_raising

    success = False
    msg = None

    try:
        # A CUDA operation that forces initialization of the device
        cuda.device_array(1)
    except CudaSupportError as e:
        success = True
        msg = e.msg

    result_queue.put((success, msg))


# Similar to cuInit_raising_test above, but for testing that the string
# returned by cuda_error() is as expected.
def initialization_error_test(result_queue):
    driver.cuInit = cuInit_raising

    success = False
    msg = None

    try:
        # A CUDA operation that forces initialization of the device
        cuda.device_array(1)
    except CudaSupportError:
        success = True

    msg = cuda.cuda_error()
    result_queue.put((success, msg))


# For testing the path where Driver.__init__() catches a CudaSupportError
def cuda_disabled_test(result_queue):
    success = False
    msg = None

    try:
        # A CUDA operation that forces initialization of the device
        cuda.device_array(1)
    except CudaSupportError as e:
        success = True
        msg = e.msg

    result_queue.put((success, msg))


# Similar to cuda_disabled_test, but checks cuda.cuda_error() instead of the
# exception raised on initialization
def cuda_disabled_error_test(result_queue):
    success = False
    msg = None

    try:
        # A CUDA operation that forces initialization of the device
        cuda.device_array(1)
    except CudaSupportError:
        success = True

    msg = cuda.cuda_error()
    result_queue.put((success, msg))


@skip_on_cudasim("CUDA Simulator does not initialize driver")
class TestInit(CUDATestCase):
    def _test_init_failure(self, target, expected):
        # Run the initialization failure test in a separate subprocess
        ctx = mp.get_context("spawn")
        result_queue = ctx.Queue()
        proc = ctx.Process(target=target, args=(result_queue,))
        proc.start()
        proc.join(30)  # should complete within 30s
        success, msg = result_queue.get()

        # Ensure the child process raised an exception during initialization
        # before checking the message
        if not success:
            self.fail("CudaSupportError not raised")

        self.assertIn(expected, msg)

    def test_init_failure_raising(self):
        expected = "Error at driver init: CUDA_ERROR_UNKNOWN (999)"
        self._test_init_failure(cuInit_raising_test, expected)

    def test_init_failure_error(self):
        expected = "CUDA_ERROR_UNKNOWN (999)"
        self._test_init_failure(initialization_error_test, expected)

    def _test_cuda_disabled(self, target):
        # Uses _test_init_failure to launch the test in a separate subprocess
        # with CUDA disabled.
        cuda_disabled = os.environ.get("NUMBA_DISABLE_CUDA")
        os.environ["NUMBA_DISABLE_CUDA"] = "1"
        try:
            expected = "CUDA is disabled due to setting NUMBA_DISABLE_CUDA=1"
            self._test_init_failure(cuda_disabled_test, expected)
        finally:
            if cuda_disabled is not None:
                os.environ["NUMBA_DISABLE_CUDA"] = cuda_disabled
            else:
                os.environ.pop("NUMBA_DISABLE_CUDA")

    def test_cuda_disabled_raising(self):
        self._test_cuda_disabled(cuda_disabled_test)

    def test_cuda_disabled_error(self):
        self._test_cuda_disabled(cuda_disabled_error_test)

    def test_init_success(self):
        # Here we assume that initialization is successful (because many bad
        # things will happen with the test suite if it is not) and check that
        # there is no error recorded.
        self.assertIsNone(cuda.cuda_error())


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