###
# 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

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 _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 Semaphore(1, gevent.get_hub())

    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

    def _do_test_acquire_in_one_then_another(self, release=True, **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.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(3):
                gevent.idle()
                if thread_acquired.wait(timing.LARGE_TICK):
                    break

            self.assertEqual(acquired, [True])

        thread_acquired.wait(timing.LARGE_TICK * 5)
        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,
            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
        ))
        gevent.idle()
        sem.release()
        glet.join()
        thread.join(timing.LARGE_TICK)

        self.assertEqual(glet.value, True)
        self.assertEqual([], exc_info)
        self.assertEqual([False], acquired)

    # 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.

@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()
