/tests/test_core/test_dispatcher.py

https://github.com/rmorshea/idom · Python · 178 lines · 111 code · 41 blank · 26 comment · 7 complexity · 80787f6c6c4f4d2b6141ab355939418e MD5 · raw file

  1. import asyncio
  2. import sys
  3. from typing import Any, Sequence
  4. import pytest
  5. import idom
  6. from idom.core.dispatcher import (
  7. VdomJsonPatch,
  8. _create_shared_view_dispatcher,
  9. create_shared_view_dispatcher,
  10. dispatch_single_view,
  11. ensure_shared_view_dispatcher_future,
  12. )
  13. from idom.core.layout import Layout, LayoutEvent, LayoutUpdate
  14. from idom.testing import StaticEventHandler
  15. EVENT_NAME = "onEvent"
  16. STATIC_EVENT_HANDLER = StaticEventHandler()
  17. def test_vdom_json_patch_create_from_apply_to():
  18. update = LayoutUpdate("", {"a": 1, "b": [1]}, {"a": 2, "b": [1, 2]})
  19. patch = VdomJsonPatch.create_from(update)
  20. result = patch.apply_to({"a": 1, "b": [1]})
  21. assert result == {"a": 2, "b": [1, 2]}
  22. def make_send_recv_callbacks(events_to_inject):
  23. changes = []
  24. # We need a semaphor here to simulate recieving an event after each update is sent.
  25. # The effect is that the send() and recv() callbacks trade off control. If we did
  26. # not do this, it would easy to determine when to halt because, while we might have
  27. # received all the events, they might not have been sent since the two callbacks are
  28. # executed in separate loops.
  29. sem = asyncio.Semaphore(0)
  30. async def send(patch):
  31. changes.append(patch)
  32. sem.release()
  33. if not events_to_inject:
  34. raise idom.Stop()
  35. async def recv():
  36. await sem.acquire()
  37. try:
  38. return events_to_inject.pop(0)
  39. except IndexError:
  40. # wait forever
  41. await asyncio.Event().wait()
  42. return changes, send, recv
  43. def make_events_and_expected_model():
  44. events = [LayoutEvent(STATIC_EVENT_HANDLER.target, [])] * 4
  45. expected_model = {
  46. "tagName": "div",
  47. "attributes": {"count": 4},
  48. "eventHandlers": {
  49. EVENT_NAME: {
  50. "target": STATIC_EVENT_HANDLER.target,
  51. "preventDefault": False,
  52. "stopPropagation": False,
  53. }
  54. },
  55. }
  56. return events, expected_model
  57. def assert_changes_produce_expected_model(
  58. changes: Sequence[LayoutUpdate],
  59. expected_model: Any,
  60. ) -> None:
  61. model_from_changes = {}
  62. for update in changes:
  63. model_from_changes = update.apply_to(model_from_changes)
  64. assert model_from_changes == expected_model
  65. @idom.component
  66. def Counter():
  67. count, change_count = idom.hooks.use_reducer(
  68. (lambda old_count, diff: old_count + diff),
  69. initial_value=0,
  70. )
  71. handler = STATIC_EVENT_HANDLER.use(lambda: change_count(1))
  72. return idom.html.div({EVENT_NAME: handler, "count": count})
  73. async def test_dispatch_single_view():
  74. events, expected_model = make_events_and_expected_model()
  75. changes, send, recv = make_send_recv_callbacks(events)
  76. await asyncio.wait_for(dispatch_single_view(Layout(Counter()), send, recv), 1)
  77. assert_changes_produce_expected_model(changes, expected_model)
  78. async def test_create_shared_state_dispatcher():
  79. events, model = make_events_and_expected_model()
  80. changes_1, send_1, recv_1 = make_send_recv_callbacks(events)
  81. changes_2, send_2, recv_2 = make_send_recv_callbacks(events)
  82. async with create_shared_view_dispatcher(Layout(Counter())) as dispatcher:
  83. dispatcher(send_1, recv_1)
  84. dispatcher(send_2, recv_2)
  85. assert_changes_produce_expected_model(changes_1, model)
  86. assert_changes_produce_expected_model(changes_2, model)
  87. async def test_ensure_shared_view_dispatcher_future():
  88. events, model = make_events_and_expected_model()
  89. changes_1, send_1, recv_1 = make_send_recv_callbacks(events)
  90. changes_2, send_2, recv_2 = make_send_recv_callbacks(events)
  91. dispatch_future, dispatch = ensure_shared_view_dispatcher_future(Layout(Counter()))
  92. await asyncio.gather(
  93. dispatch(send_1, recv_1),
  94. dispatch(send_2, recv_2),
  95. return_exceptions=True,
  96. )
  97. # the dispatch future should run forever, until cancelled
  98. with pytest.raises(asyncio.TimeoutError):
  99. await asyncio.wait_for(dispatch_future, timeout=1)
  100. dispatch_future.cancel()
  101. await asyncio.gather(dispatch_future, return_exceptions=True)
  102. assert_changes_produce_expected_model(changes_1, model)
  103. assert_changes_produce_expected_model(changes_2, model)
  104. async def test_private_create_shared_view_dispatcher_cleans_up_patch_queues():
  105. """Report an issue if this test breaks
  106. Some internals of idom.core.dispatcher may need to be changed in order to make some
  107. internal state easier to introspect.
  108. Ideally we would just check if patch queues are getting cleaned up more directly,
  109. but without having access to that, we use some side effects to try and infer whether
  110. it happens.
  111. """
  112. @idom.component
  113. def SomeComponent():
  114. return idom.html.div()
  115. async def send(patch):
  116. raise idom.Stop()
  117. async def recv():
  118. return LayoutEvent("something", [])
  119. with idom.Layout(SomeComponent()) as layout:
  120. dispatch_shared_view, push_patch = await _create_shared_view_dispatcher(layout)
  121. # Dispatch a view that should exit. After exiting its patch queue should be
  122. # cleaned up and removed. Since we only dispatched one view there should be
  123. # no patch queues.
  124. await dispatch_shared_view(send, recv) # this should stop immediately
  125. # We create a patch and check its ref count. We will check this after attempting
  126. # to push out the change.
  127. patch = VdomJsonPatch("anything", [])
  128. patch_ref_count = sys.getrefcount(patch)
  129. # We push out this change.
  130. push_patch(patch)
  131. # Because there should be no patch queues, we expect that the ref count remains
  132. # the same. If the ref count had increased, then we would know that the patch
  133. # queue has not been cleaned up and that the patch we just pushed was added to
  134. # it.
  135. assert not sys.getrefcount(patch) > patch_ref_count