mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
[App] Integration tests now work on Windows too
This commit is contained in:
@@ -13,7 +13,8 @@ from unittest import mock
|
||||
from rich.console import Console
|
||||
|
||||
from textual import events, errors
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.app import App, ComposeResult, WINDOWS
|
||||
from textual._context import active_app
|
||||
from textual.driver import Driver
|
||||
from textual.geometry import Size
|
||||
|
||||
@@ -77,7 +78,7 @@ class AppTest(App):
|
||||
self,
|
||||
*,
|
||||
time_mocking_ticks_granularity_fps: int = 60, # i.e. when moving forward by 1 second we'll do it though 60 ticks
|
||||
waiting_duration_after_initialisation: float = 0.1,
|
||||
waiting_duration_after_initialisation: float = 1,
|
||||
waiting_duration_after_yield: float = 0,
|
||||
) -> AsyncContextManager[MockedTimeMoveClockForward]:
|
||||
async def run_app() -> None:
|
||||
@@ -85,50 +86,35 @@ class AppTest(App):
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def get_running_state_context_manager():
|
||||
self._set_active()
|
||||
|
||||
with mock_textual_timers(
|
||||
ticks_granularity_fps=time_mocking_ticks_granularity_fps
|
||||
) as move_time_forward:
|
||||
) as move_clock_forward:
|
||||
run_task = asyncio.create_task(run_app())
|
||||
await asyncio.sleep(0.001)
|
||||
# timeout_before_yielding_task = asyncio.create_task(
|
||||
# asyncio.sleep(waiting_duration_after_initialisation)
|
||||
# )
|
||||
# done, pending = await asyncio.wait(
|
||||
# (
|
||||
# run_task,
|
||||
# timeout_before_yielding_task,
|
||||
# ),
|
||||
# return_when=asyncio.FIRST_COMPLETED,
|
||||
# )
|
||||
# if run_task in done or run_task not in pending:
|
||||
# raise RuntimeError(
|
||||
# "TestApp is no longer running after its initialization period"
|
||||
# )
|
||||
|
||||
await move_time_forward(seconds=waiting_duration_after_initialisation)
|
||||
# We have to do this because `run_app()` is running in its own async task, and our test is going to
|
||||
# run in this one - so the app must also be the active App in our current context:
|
||||
self._set_active()
|
||||
|
||||
await move_clock_forward(seconds=waiting_duration_after_initialisation)
|
||||
# make sure the App has entered its main loop at this stage:
|
||||
assert self._driver is not None
|
||||
|
||||
self.force_screen_update()
|
||||
await self.force_screen_update()
|
||||
|
||||
yield move_time_forward
|
||||
# And now it's time to pass the torch on to the test function!
|
||||
# We provide the `move_clock_forward` function to it,
|
||||
# so it can also do some time-based Textual stuff if it needs to:
|
||||
yield move_clock_forward
|
||||
|
||||
await move_time_forward(seconds=waiting_duration_after_yield)
|
||||
await move_clock_forward(seconds=waiting_duration_after_yield)
|
||||
|
||||
self.force_screen_update()
|
||||
# waiting_duration = max(
|
||||
# waiting_duration_post_yield or 0,
|
||||
# self.screen._update_timer._interval,
|
||||
# )
|
||||
# await asyncio.sleep(waiting_duration)
|
||||
# Make sure our screen is up to date before exiting the context manager,
|
||||
# so tests using our `last_display_capture` for example can assert things on an up to date screen:
|
||||
await self.force_screen_update()
|
||||
|
||||
# if force_timers_tick_after_yield:
|
||||
# await textual_timers_force_tick()
|
||||
|
||||
assert not run_task.done()
|
||||
await self.shutdown()
|
||||
# End of simulated time: we just shut down ourselves:
|
||||
assert not run_task.done()
|
||||
await self.shutdown()
|
||||
|
||||
return get_running_state_context_manager()
|
||||
|
||||
@@ -175,12 +161,17 @@ class AppTest(App):
|
||||
return segment.text[0]
|
||||
return ""
|
||||
|
||||
def force_screen_update(self, *, repaint: bool = True, layout: bool = True) -> None:
|
||||
async def force_screen_update(
|
||||
self, *, repaint: bool = True, layout: bool = True
|
||||
) -> None:
|
||||
try:
|
||||
self.screen.refresh(repaint=repaint, layout=layout)
|
||||
self.screen._on_update()
|
||||
screen = self.screen
|
||||
except IndexError:
|
||||
pass # the app may not have a screen yet
|
||||
return # the app may not have a screen yet
|
||||
screen.refresh(repaint=repaint, layout=layout)
|
||||
screen._on_update()
|
||||
|
||||
await let_asyncio_process_some_events()
|
||||
|
||||
def on_exception(self, error: Exception) -> None:
|
||||
# In tests we want the errors to be raised, rather than printed to a Console
|
||||
@@ -191,6 +182,10 @@ class AppTest(App):
|
||||
"Use `async with my_test_app.in_running_state()` rather than `my_test_app.run()`"
|
||||
)
|
||||
|
||||
@property
|
||||
def active_app(self) -> App | None:
|
||||
return active_app.get()
|
||||
|
||||
@property
|
||||
def total_capture(self) -> str | None:
|
||||
return self.console.file.getvalue()
|
||||
@@ -259,6 +254,16 @@ class DriverTest(Driver):
|
||||
pass
|
||||
|
||||
|
||||
# > The resolution of the monotonic clock on Windows is usually around 15.6 msec.
|
||||
# > The best resolution is 0.5 msec.
|
||||
# @link https://docs.python.org/3/library/asyncio-platforms.html:
|
||||
ASYNCIO_EVENTS_PROCESSING_REQUIRED_PERIOD = 0.025 if WINDOWS else 0.002
|
||||
|
||||
|
||||
async def let_asyncio_process_some_events() -> None:
|
||||
await asyncio.sleep(ASYNCIO_EVENTS_PROCESSING_REQUIRED_PERIOD)
|
||||
|
||||
|
||||
def mock_textual_timers(
|
||||
*,
|
||||
ticks_granularity_fps: int = 60,
|
||||
@@ -278,11 +283,17 @@ def mock_textual_timers(
|
||||
target_event_monotonic_time = current_time + duration
|
||||
pending_sleep_events.append((target_event_monotonic_time, event))
|
||||
# Ok, let's wait for this Event
|
||||
# - which can only be "unlocked" by calls to `move_clock_forward()`
|
||||
# (which can only be "unlocked" by calls to `move_clock_forward()`)
|
||||
await event.wait()
|
||||
|
||||
# Our replacement for "textual._timer.Timer.get_time" and "textual.message.Message._get_time":
|
||||
def get_time_mock() -> float:
|
||||
nonlocal current_time
|
||||
|
||||
# let's make the time advance slightly between 2 consecutive calls of this function,
|
||||
# within the same order of magnitude than 2 consecutive calls to ` timer.monotonic()`:
|
||||
current_time += 1.1e-06
|
||||
|
||||
return current_time
|
||||
|
||||
async def move_clock_forward(*, seconds: float) -> tuple[float, int]:
|
||||
@@ -292,11 +303,14 @@ def mock_textual_timers(
|
||||
activated_timers_count_total = 0
|
||||
for tick_counter in range(ticks_count):
|
||||
current_time += single_tick_duration
|
||||
activated_timers_count_total += check_sleep_timers_to_activate()
|
||||
activated_timers_count = check_sleep_timers_to_activate()
|
||||
activated_timers_count_total += activated_timers_count
|
||||
# Let's give an opportunity to asyncio-related stuff to happen,
|
||||
# now that we likely unlocked some occurrences of `await sleep(duration)`:
|
||||
if activated_timers_count:
|
||||
await let_asyncio_process_some_events()
|
||||
|
||||
# Let's give an opportunity to asyncio-related stuff to happen,
|
||||
# now that we unlocked some occurrences of `await sleep(duration)`:
|
||||
await asyncio.sleep(0.0001)
|
||||
await let_asyncio_process_some_events()
|
||||
|
||||
return current_time, activated_timers_count_total
|
||||
|
||||
@@ -307,8 +321,8 @@ def mock_textual_timers(
|
||||
for i, (target_event_monotonic_time, event) in enumerate(
|
||||
pending_sleep_events
|
||||
):
|
||||
if target_event_monotonic_time < current_time:
|
||||
continue
|
||||
if current_time < target_event_monotonic_time:
|
||||
continue # not time for you yet, dear awaiter...
|
||||
# Right, let's release this waiting event!
|
||||
event.set()
|
||||
activated_timers_count += 1
|
||||
|
||||
Reference in New Issue
Block a user