###
# This file is test__semaphore.py only for organization purposes.
# The public API,
# and the *only* correct place to import Semaphore --- even in tests ---
# is ``gevent.lock``, never ``gevent._semaphore``.
##
from __future__ import print_function
from __future__ import absolute_import

import weakref

import gevent
import gevent.exceptions
from gevent.lock import Semaphore
from gevent.lock import BoundedSemaphore

import gevent.testing as greentest
from gevent.testing import timing

class TestSemaphore(greentest.TestCase):

    # issue 39
    def test_acquire_returns_false_after_timeout(self):
        s = Semaphore(value=0)
        result = s.acquire(timeout=0.01)
        assert result is False, repr(result)

    def test_release_twice(self):
        s = Semaphore()
        result = []
        s.rawlink(lambda s: result.append('a'))
        s.release()
        s.rawlink(lambda s: result.append('b'))
        s.release()
        gevent.sleep(0.001)
        # The order, though, is not guaranteed.
        self.assertEqual(sorted(result), ['a', 'b'])

    def test_semaphore_weakref(self):
        s = Semaphore()
        r = weakref.ref(s)
        self.assertEqual(s, r())

    @greentest.ignores_leakcheck
    def test_semaphore_in_class_with_del(self):
        # Issue #704. This used to crash the process
        # under PyPy through at least 4.0.1 if the Semaphore
        # was implemented with Cython.
        class X(object):
            def __init__(self):
                self.s = Semaphore()

            def __del__(self):
                self.s.acquire()

        X()
        import gc
        gc.collect()
        gc.collect()


    def test_rawlink_on_unacquired_runs_notifiers(self):
        # https://github.com/gevent/gevent/issues/1287

        # Rawlinking a ready semaphore should fire immediately,
        # not raise LoopExit
        s = Semaphore()
        gevent.wait([s])


class TestSemaphoreMultiThread(greentest.TestCase):
    # Tests that the object can be acquired correctly across
    # multiple threads.
    # Used as a base class.

    # See https://github.com/gevent/gevent/issues/1437

    def _getTargetClass(self):
        return Semaphore

    def _makeOne(self):
        # Create an object that is associated with the current hub. If
        # we don't do this now, it gets initialized lazily the first
        # time it would have to block, which, in the event of threads,
        # would be from an arbitrary thread.
        return self._getTargetClass()(1)

    def _makeThreadMain(self, thread_running, thread_acquired, sem,
                        acquired, exc_info,
                        **thread_acquire_kwargs):
        from gevent._hub_local import get_hub_if_exists
        import sys

        def thread_main():
            thread_running.set()
            try:
                acquired.append(
                    sem.acquire(**thread_acquire_kwargs)
                )
            except:
                exc_info[:] = sys.exc_info()
                raise # Print
            finally:
                hub = get_hub_if_exists()
                if hub is not None:
                    hub.join()
                    hub.destroy(destroy_loop=True)
                thread_acquired.set()
        return thread_main

    IDLE_ITERATIONS = 5

    def _do_test_acquire_in_one_then_another(self,
                                             release=True,
                                             require_thread_acquired_to_finish=False,
                                             **thread_acquire_kwargs):
        from gevent import monkey
        self.assertFalse(monkey.is_module_patched('threading'))

        import threading
        thread_running = threading.Event()
        thread_acquired = threading.Event()

        sem = self._makeOne()
        # Make future acquires block
        sem.acquire()

        exc_info = []
        acquired = []

        t = threading.Thread(target=self._makeThreadMain(
            thread_running, thread_acquired, sem,
            acquired, exc_info,
            **thread_acquire_kwargs
        ))
        t.daemon = True
        t.start()
        thread_running.wait(10) # implausibly large time
        if release:
            sem.release()
            # Spin the loop to be sure the release gets through.
            # (Release schedules the notifier to run, and when the
            # notifier run it sends the async notification to the
            # other thread. Depending on exactly where we are in the
            # event loop, and the limit to the number of callbacks
            # that get run (including time-based) the notifier may or
            # may not be immediately ready to run, so this can take up
            # to two iterations.)
            for _ in range(self.IDLE_ITERATIONS):
                gevent.idle()
                if thread_acquired.wait(timing.LARGE_TICK):
                    break

            self.assertEqual(acquired, [True])

        if not release and thread_acquire_kwargs.get("timeout"):
            # Spin the loop to be sure that the timeout has a chance to
            # process. Interleave this with something that drops the GIL
            # so the background thread has a chance to notice that.
            for _ in range(self.IDLE_ITERATIONS):
                gevent.idle()
                if thread_acquired.wait(timing.LARGE_TICK):
                    break
        thread_acquired.wait(timing.LARGE_TICK * 5)

        if require_thread_acquired_to_finish:
            self.assertTrue(thread_acquired.is_set())
        try:
            self.assertEqual(exc_info, [])
        finally:
            exc_info = None

        return sem, acquired

    def test_acquire_in_one_then_another(self):
        self._do_test_acquire_in_one_then_another(release=True)

    def test_acquire_in_one_then_another_timed(self):
        sem, acquired_in_thread = self._do_test_acquire_in_one_then_another(
            release=False,
            require_thread_acquired_to_finish=True,
            timeout=timing.SMALLEST_RELIABLE_DELAY)
        self.assertEqual([False], acquired_in_thread)
        # This doesn't, of course, notify anything, because
        # the waiter has given up.
        sem.release()
        notifier = getattr(sem, '_notifier', None)
        self.assertIsNone(notifier)

    def test_acquire_in_one_wait_greenlet_wait_thread_gives_up(self):
        # The waiter in the thread both arrives and gives up while
        # the notifier is already running...or at least, that's what
        # we'd like to arrange, but the _notify_links function doesn't
        # drop the GIL/object lock, so the other thread is stuck and doesn't
        # actually get to call into the acquire method.

        from gevent import monkey
        self.assertFalse(monkey.is_module_patched('threading'))

        import threading

        sem = self._makeOne()
        # Make future acquires block
        sem.acquire()

        def greenlet_one():
            ack = sem.acquire()
            # We're running in the notifier function right now. It switched to
            # us.
            thread.start()
            gevent.sleep(timing.LARGE_TICK)
            return ack

        exc_info = []
        acquired = []

        glet = gevent.spawn(greenlet_one)
        thread = threading.Thread(target=self._makeThreadMain(
            threading.Event(), threading.Event(),
            sem,
            acquired, exc_info,
            timeout=timing.LARGE_TICK
        ))
        thread.daemon = True
        gevent.idle()
        sem.release()
        glet.join()
        for _ in range(3):
            gevent.idle()
            thread.join(timing.LARGE_TICK)

        self.assertEqual(glet.value, True)
        self.assertEqual([], exc_info)
        self.assertEqual([False], acquired)
        self.assertTrue(glet.dead, glet)
        glet = None

    def assertOneHasNoHub(self, sem):
        self.assertIsNone(sem.hub, sem)

    @greentest.skipOnPyPyOnWindows("Flaky there; can't reproduce elsewhere")
    def test_dueling_threads(self, acquire_args=(), create_hub=None):
        # pylint:disable=too-many-locals,too-many-statements

        # Threads doing nothing but acquiring and releasing locks, without
        # having any other greenlets to switch to.
        # https://github.com/gevent/gevent/issues/1698
        from gevent import monkey
        from gevent._hub_local import get_hub_if_exists

        self.assertFalse(monkey.is_module_patched('threading'))

        import threading
        from time import sleep as native_sleep

        sem = self._makeOne()
        self.assertOneHasNoHub(sem)
        count = 10000
        results = [-1, -1]
        run = True
        def do_it(ix):
            if create_hub:
                gevent.get_hub()

            try:
                for i in range(count):
                    if not run:
                        break

                    sem.acquire(*acquire_args)
                    sem.release()
                    results[ix] = i
                    if not create_hub:
                        # We don't artificially create the hub.
                        self.assertIsNone(
                            get_hub_if_exists(),
                            (get_hub_if_exists(), ix, i)
                        )
                    if create_hub and i % 10 == 0:
                        gevent.sleep(timing.SMALLEST_RELIABLE_DELAY)
                    elif i % 100 == 0:
                        native_sleep(timing.SMALLEST_RELIABLE_DELAY)
            except Exception as ex: # pylint:disable=broad-except
                import traceback; traceback.print_exc()
                results[ix] = str(ex)
                ex = None
            finally:
                hub = get_hub_if_exists()
                if hub is not None:
                    hub.join()
                    hub.destroy(destroy_loop=True)

        t1 = threading.Thread(target=do_it, args=(0,))
        t1.daemon = True
        t2 = threading.Thread(target=do_it, args=(1,))
        t2.daemon = True
        t1.start()
        t2.start()

        t1.join(1)
        t2.join(1)

        while t1.is_alive() or t2.is_alive():
            cur = list(results)
            t1.join(7)
            t2.join(7)
            if cur == results:
                # Hmm, after two seconds, no progress
                run = False
                break

        self.assertEqual(results, [count - 1, count - 1])

    def test_dueling_threads_timeout(self):
        self.test_dueling_threads((True, 4))

    def test_dueling_threads_with_hub(self):
        self.test_dueling_threads(create_hub=True)


    # XXX: Need a test with multiple greenlets in a non-primary
    # thread. Things should work, just very slowly; instead of moving through
    # greenlet.switch(), they'll be moving with async watchers.

class TestBoundedSemaphoreMultiThread(TestSemaphoreMultiThread):

    def _getTargetClass(self):
        return BoundedSemaphore

@greentest.skipOnPurePython("Needs C extension")
class TestCExt(greentest.TestCase):

    def test_c_extension(self):
        self.assertEqual(Semaphore.__module__,
                         'gevent._gevent_c_semaphore')


class SwitchWithFixedHash(object):
    # Replaces greenlet.switch with a callable object
    # with a hash code we control. This only matters if
    # we're hashing this somewhere (which we used to), but
    # that doesn't preserve order, so we don't do
    # that anymore.

    def __init__(self, greenlet, hashcode):
        self.switch = greenlet.switch
        self.hashcode = hashcode

    def __hash__(self):
        raise AssertionError

    def __eq__(self, other):
        raise AssertionError

    def __call__(self, *args, **kwargs):
        return self.switch(*args, **kwargs)

    def __repr__(self):
        return repr(self.switch)

class FirstG(gevent.Greenlet):
    # A greenlet whose switch method will have a low hashcode.

    hashcode = 10

    def __init__(self, *args, **kwargs):
        gevent.Greenlet.__init__(self, *args, **kwargs)
        self.switch = SwitchWithFixedHash(self, self.hashcode)


class LastG(FirstG):
    # A greenlet whose switch method will have a high hashcode.
    hashcode = 12


def acquire_then_exit(sem, should_quit):
    sem.acquire()
    should_quit.append(True)


def acquire_then_spawn(sem, should_quit):
    if should_quit:
        return
    sem.acquire()
    g = FirstG.spawn(release_then_spawn, sem, should_quit)
    g.join()

def release_then_spawn(sem, should_quit):
    sem.release()
    if should_quit: # pragma: no cover
        return
    g = FirstG.spawn(acquire_then_spawn, sem, should_quit)
    g.join()

class TestSemaphoreFair(greentest.TestCase):

    def test_fair_or_hangs(self):
        # If the lock isn't fair, this hangs, spinning between
        # the last two greenlets.
        # See https://github.com/gevent/gevent/issues/1487
        sem = Semaphore()
        should_quit = []

        keep_going1 = FirstG.spawn(acquire_then_spawn, sem, should_quit)
        keep_going2 = FirstG.spawn(acquire_then_spawn, sem, should_quit)
        exiting = LastG.spawn(acquire_then_exit, sem, should_quit)

        with self.assertRaises(gevent.exceptions.LoopExit):
            gevent.joinall([keep_going1, keep_going2, exiting])

        self.assertTrue(exiting.dead, exiting)
        self.assertTrue(keep_going2.dead, keep_going2)
        self.assertFalse(keep_going1.dead, keep_going1)

        sem.release()
        keep_going1.kill()
        keep_going2.kill()
        exiting.kill()

        gevent.idle()

if __name__ == '__main__':
    greentest.main()
