|
| 1 | +"""Regression coverage for gevent monkey-patching compatibility.""" |
| 2 | + |
| 3 | +import importlib.util |
| 4 | +import subprocess |
| 5 | +import sys |
| 6 | +import textwrap |
| 7 | +import unittest |
| 8 | +from queue import Full, Queue |
| 9 | +from unittest import mock |
| 10 | + |
| 11 | +from posthog.client import Client, _new_lane_queue |
| 12 | +from posthog.test.test_utils import FAKE_TEST_API_KEY |
| 13 | + |
| 14 | + |
| 15 | +class TestLaneQueueFallback(unittest.TestCase): |
| 16 | + def test_uses_working_queue_without_loading_gevent(self): |
| 17 | + with mock.patch.dict(sys.modules, {"gevent.monkey": None}): |
| 18 | + queue = _new_lane_queue(10) |
| 19 | + |
| 20 | + self.assertIsInstance(queue, Queue) |
| 21 | + |
| 22 | + def test_disables_capture_if_no_compatible_queue_is_available(self): |
| 23 | + incompatible_queue = mock.Mock(spec=[]) |
| 24 | + |
| 25 | + with self.assertLogs("posthog", level="ERROR") as logs: |
| 26 | + with ( |
| 27 | + mock.patch("posthog.client.Queue", return_value=incompatible_queue), |
| 28 | + mock.patch.dict(sys.modules, {"gevent.monkey": None}), |
| 29 | + ): |
| 30 | + queue = _new_lane_queue(10) |
| 31 | + |
| 32 | + with self.assertRaises(Full): |
| 33 | + queue.put("event", block=False) |
| 34 | + self.assertTrue(queue.empty()) |
| 35 | + self.assertEqual(queue.unfinished_tasks, 0) |
| 36 | + self.assertIsNone(queue.task_done()) |
| 37 | + self.assertIn("gevent.monkey is not loaded", logs.output[0]) |
| 38 | + self.assertIn("disabling asynchronous capture for the lane", logs.output[0]) |
| 39 | + |
| 40 | + def test_logs_gevent_recovery_failure_before_disabling(self): |
| 41 | + incompatible_queue = mock.Mock(spec=[]) |
| 42 | + monkey = mock.Mock() |
| 43 | + monkey.is_object_patched.return_value = True |
| 44 | + monkey.get_original.side_effect = RuntimeError("broken gevent state") |
| 45 | + |
| 46 | + with self.assertLogs("posthog", level="ERROR") as logs: |
| 47 | + with ( |
| 48 | + mock.patch("posthog.client.Queue", return_value=incompatible_queue), |
| 49 | + mock.patch.dict(sys.modules, {"gevent.monkey": monkey}), |
| 50 | + ): |
| 51 | + queue = _new_lane_queue(10) |
| 52 | + |
| 53 | + self.assertTrue(queue.empty()) |
| 54 | + self.assertIn("Failed to restore the original queue.Queue", logs.output[0]) |
| 55 | + self.assertIn("broken gevent state", logs.output[0]) |
| 56 | + |
| 57 | + def test_disables_only_async_capture_if_no_compatible_queue_is_available(self): |
| 58 | + incompatible_queue = mock.Mock(spec=[]) |
| 59 | + |
| 60 | + with self.assertLogs("posthog", level="ERROR"): |
| 61 | + with ( |
| 62 | + mock.patch("posthog.client.Queue", return_value=incompatible_queue), |
| 63 | + mock.patch.dict(sys.modules, {"gevent.monkey": None}), |
| 64 | + ): |
| 65 | + client = Client(FAKE_TEST_API_KEY) |
| 66 | + |
| 67 | + self.assertFalse(client.disabled) |
| 68 | + self.assertFalse(client._analytics_lane.available) |
| 69 | + self.assertFalse(client._ai_lane.available) |
| 70 | + self.assertEqual(client.consumers, []) |
| 71 | + self.assertIsNone(client.capture("disabled-queue", distinct_id="distinct_id")) |
| 72 | + self.assertEqual(client.consumers, []) |
| 73 | + client.flush() |
| 74 | + client.shutdown() |
| 75 | + |
| 76 | + def test_incompatible_queue_does_not_disable_queue_independent_capabilities(self): |
| 77 | + incompatible_queue = mock.Mock(spec=[]) |
| 78 | + |
| 79 | + with self.assertLogs("posthog", level="ERROR"): |
| 80 | + with ( |
| 81 | + mock.patch("posthog.client.Queue", return_value=incompatible_queue), |
| 82 | + mock.patch.dict(sys.modules, {"gevent.monkey": None}), |
| 83 | + mock.patch("posthog.client.batch_post") as mock_post, |
| 84 | + mock.patch( |
| 85 | + "posthog.client.flags", |
| 86 | + return_value={"featureFlags": {"beta-feature": True}}, |
| 87 | + ) as mock_flags, |
| 88 | + ): |
| 89 | + client = Client(FAKE_TEST_API_KEY, sync_mode=True) |
| 90 | + event_uuid = client.capture("sync-capture", distinct_id="distinct_id") |
| 91 | + decision = client.get_flags_decision("distinct_id") |
| 92 | + |
| 93 | + self.assertFalse(client.disabled) |
| 94 | + self.assertIsNotNone(event_uuid) |
| 95 | + mock_post.assert_called_once() |
| 96 | + self.assertTrue(decision["flags"]["beta-feature"].enabled) |
| 97 | + mock_flags.assert_called_once() |
| 98 | + |
| 99 | + |
| 100 | +@unittest.skipUnless(importlib.util.find_spec("gevent"), "gevent is not installed") |
| 101 | +class TestGeventCompatibility(unittest.TestCase): |
| 102 | + def test_capture_and_flush_after_monkey_patching(self): |
| 103 | + script = textwrap.dedent( |
| 104 | + """ |
| 105 | + import gevent.monkey |
| 106 | +
|
| 107 | + gevent.monkey.patch_all() |
| 108 | +
|
| 109 | + import queue |
| 110 | +
|
| 111 | + assert gevent.monkey.is_object_patched("queue", "Queue"), ( |
| 112 | + "gevent did not replace queue.Queue; the regression scenario " |
| 113 | + "is not being exercised" |
| 114 | + ) |
| 115 | +
|
| 116 | + from unittest import mock |
| 117 | +
|
| 118 | + with mock.patch("posthog.consumer.batch_post") as mock_post: |
| 119 | + from posthog.client import Client |
| 120 | +
|
| 121 | + client = Client("phc_test", flush_at=1, flush_interval=60) |
| 122 | + original_queue = gevent.monkey.get_original("queue", "Queue") |
| 123 | + assert isinstance(client.queue, original_queue) |
| 124 | + assert not isinstance(client.queue, queue.Queue) |
| 125 | + for attribute in ( |
| 126 | + "mutex", |
| 127 | + "not_empty", |
| 128 | + "not_full", |
| 129 | + "all_tasks_done", |
| 130 | + "unfinished_tasks", |
| 131 | + "_qsize", |
| 132 | + "_get", |
| 133 | + ): |
| 134 | + assert hasattr(client.queue, attribute), attribute |
| 135 | +
|
| 136 | + client.capture("gevent-regression", distinct_id="distinct_id") |
| 137 | + client.flush(timeout_seconds=10) |
| 138 | + client.join() |
| 139 | +
|
| 140 | + assert mock_post.called, "batch_post was never called" |
| 141 | + assert client.queue.empty(), "flush did not drain the queue" |
| 142 | + """ |
| 143 | + ) |
| 144 | + |
| 145 | + result = subprocess.run( |
| 146 | + [sys.executable, "-c", script], |
| 147 | + capture_output=True, |
| 148 | + text=True, |
| 149 | + timeout=60, |
| 150 | + ) |
| 151 | + |
| 152 | + self.assertEqual(result.returncode, 0, result.stderr) |
0 commit comments