Skip to content

Commit edcca09

Browse files
authored
First prototype of native pytest plugin for launch based tests (#528)
Signed-off-by: Ivan Santiago Paunovic <[email protected]>
1 parent be36e50 commit edcca09

31 files changed

+2131
-49
lines changed

launch/launch/actions/execute_local.py

Lines changed: 89 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import asyncio
1818
import io
19+
import logging
1920
import os
2021
import platform
2122
import signal
@@ -91,6 +92,7 @@ def __init__(
9192
emulate_tty: bool = False,
9293
output: Text = 'log',
9394
output_format: Text = '[{this.process_description.final_name}] {line}',
95+
cached_output: bool = False,
9496
log_cmd: bool = False,
9597
on_exit: Optional[Union[
9698
SomeActionsType,
@@ -176,6 +178,8 @@ def __init__(
176178
:param: log_cmd if True, prints the final cmd before executing the
177179
process, which is useful for debugging when substitutions are
178180
involved.
181+
:param: cached_output if `True`, both stdout and stderr will be cached.
182+
Use get_stdout() and get_stderr() to read the buffered output.
179183
:param: on_exit list of actions to execute upon process exit.
180184
:param: respawn if 'True', relaunch the process that abnormally died.
181185
Defaults to 'False'.
@@ -191,6 +195,7 @@ def __init__(
191195
self.__output_format = output_format
192196

193197
self.__log_cmd = log_cmd
198+
self.__cached_output = cached_output
194199
self.__on_exit = on_exit
195200
self.__respawn = respawn
196201
self.__respawn_delay = respawn_delay
@@ -329,59 +334,32 @@ def __on_process_stdin(
329334
cast(ProcessStdin, event)
330335
return None
331336

332-
def __on_process_stdout(
333-
self, event: ProcessIO
337+
def __on_process_output(
338+
self, event: ProcessIO, buffer: io.TextIOBase, logger: logging.Logger
334339
) -> Optional[SomeActionsType]:
335340
to_write = event.text.decode(errors='replace')
336-
if self.__stdout_buffer.closed:
337-
# __stdout_buffer was probably closed by __flush_buffers on shutdown. Output without
341+
if buffer.closed:
342+
# buffer was probably closed by __flush_buffers on shutdown. Output without
338343
# buffering.
339-
self.__stdout_logger.info(
340-
self.__output_format.format(line=to_write, this=self)
341-
)
342-
else:
343-
self.__stdout_buffer.write(to_write)
344-
self.__stdout_buffer.seek(0)
345-
last_line = None
346-
for line in self.__stdout_buffer:
347-
if line.endswith(os.linesep):
348-
self.__stdout_logger.info(
349-
self.__output_format.format(line=line[:-len(os.linesep)], this=self)
350-
)
351-
else:
352-
last_line = line
353-
break
354-
self.__stdout_buffer.seek(0)
355-
self.__stdout_buffer.truncate(0)
356-
if last_line is not None:
357-
self.__stdout_buffer.write(last_line)
358-
359-
def __on_process_stderr(
360-
self, event: ProcessIO
361-
) -> Optional[SomeActionsType]:
362-
to_write = event.text.decode(errors='replace')
363-
if self.__stderr_buffer.closed:
364-
# __stderr buffer was probably closed by __flush_buffers on shutdown. Output without
365-
# buffering.
366-
self.__stderr_logger.info(
344+
buffer.info(
367345
self.__output_format.format(line=to_write, this=self)
368346
)
369347
else:
370-
self.__stderr_buffer.write(to_write)
371-
self.__stderr_buffer.seek(0)
348+
buffer.write(to_write)
349+
buffer.seek(0)
372350
last_line = None
373-
for line in self.__stderr_buffer:
351+
for line in buffer:
374352
if line.endswith(os.linesep):
375-
self.__stderr_logger.info(
353+
logger.info(
376354
self.__output_format.format(line=line[:-len(os.linesep)], this=self)
377355
)
378356
else:
379357
last_line = line
380358
break
381-
self.__stderr_buffer.seek(0)
382-
self.__stderr_buffer.truncate(0)
359+
buffer.seek(0)
360+
buffer.truncate(0)
383361
if last_line is not None:
384-
self.__stderr_buffer.write(last_line)
362+
buffer.write(last_line)
385363

386364
def __flush_buffers(self, event, context):
387365
line = self.__stdout_buffer.getvalue()
@@ -407,6 +385,35 @@ def __flush_buffers(self, event, context):
407385
self.__stderr_buffer.seek(0)
408386
self.__stderr_buffer.truncate(0)
409387

388+
def __on_process_output_cached(
389+
self, event: ProcessIO, buffer, logger
390+
) -> Optional[SomeActionsType]:
391+
to_write = event.text.decode(errors='replace')
392+
last_cursor = buffer.tell()
393+
buffer.seek(0, os.SEEK_END) # go to end of buffer
394+
buffer.write(to_write)
395+
buffer.seek(last_cursor)
396+
new_cursor = last_cursor
397+
for line in buffer:
398+
if not line.endswith(os.linesep):
399+
break
400+
new_cursor = buffer.tell()
401+
logger.info(
402+
self.__output_format.format(line=line[:-len(os.linesep)], this=self)
403+
)
404+
buffer.seek(new_cursor)
405+
406+
def __flush_cached_buffers(self, event, context):
407+
for line in self.__stdout_buffer:
408+
self.__stdout_logger.info(
409+
self.__output_format.format(line=line, this=self)
410+
)
411+
412+
for line in self.__stderr_buffer:
413+
self.__stderr_logger.info(
414+
self.__output_format.format(line=line, this=self)
415+
)
416+
410417
def __on_shutdown(self, event: Event, context: LaunchContext) -> Optional[SomeActionsType]:
411418
due_to_sigint = cast(Shutdown, event).due_to_sigint
412419
return self._shutdown_process(
@@ -614,6 +621,13 @@ def execute(self, context: LaunchContext) -> Optional[List[LaunchDescriptionEnti
614621
# If shutdown starts before execution can start, don't start execution.
615622
return None
616623

624+
if self.__cached_output:
625+
on_output_method = self.__on_process_output_cached
626+
flush_buffers_method = self.__flush_cached_buffers
627+
else:
628+
on_output_method = self.__on_process_output
629+
flush_buffers_method = self.__flush_buffers
630+
617631
event_handlers = [
618632
EventHandler(
619633
matcher=lambda event: is_a_subclass(event, ShutdownProcess),
@@ -626,8 +640,10 @@ def execute(self, context: LaunchContext) -> Optional[List[LaunchDescriptionEnti
626640
OnProcessIO(
627641
target_action=self,
628642
on_stdin=self.__on_process_stdin,
629-
on_stdout=self.__on_process_stdout,
630-
on_stderr=self.__on_process_stderr
643+
on_stdout=lambda event: on_output_method(
644+
event, self.__stdout_buffer, self.__stdout_logger),
645+
on_stderr=lambda event: on_output_method(
646+
event, self.__stderr_buffer, self.__stderr_logger),
631647
),
632648
OnShutdown(
633649
on_shutdown=self.__on_shutdown,
@@ -638,7 +654,7 @@ def execute(self, context: LaunchContext) -> Optional[List[LaunchDescriptionEnti
638654
),
639655
OnProcessExit(
640656
target_action=self,
641-
on_exit=self.__flush_buffers,
657+
on_exit=flush_buffers_method,
642658
),
643659
]
644660
for event_handler in event_handlers:
@@ -660,3 +676,34 @@ def execute(self, context: LaunchContext) -> Optional[List[LaunchDescriptionEnti
660676
def get_asyncio_future(self) -> Optional[asyncio.Future]:
661677
"""Return an asyncio Future, used to let the launch system know when we're done."""
662678
return self.__completed_future
679+
680+
def get_stdout(self):
681+
"""
682+
Get cached stdout.
683+
684+
:raises RuntimeError: if cached_output is false.
685+
"""
686+
if not self.__cached_output:
687+
raise RuntimeError(
688+
'cached output must be true to be able to get stdout,'
689+
f" proc '{self.__process_description.name}'")
690+
return self.__stdout_buffer.getvalue()
691+
692+
def get_stderr(self):
693+
"""
694+
Get cached stdout.
695+
696+
:raises RuntimeError: if cached_output is false.
697+
"""
698+
if not self.__cached_output:
699+
raise RuntimeError(
700+
'cached output must be true to be able to get stderr, proc'
701+
f" '{self.__process_description.name}'")
702+
return self.__stderr_buffer.getvalue()
703+
704+
@property
705+
def return_code(self):
706+
"""Get the process return code, None if it hasn't finished."""
707+
if self._subprocess_transport is None:
708+
return None
709+
return self._subprocess_transport.get_returncode()

launch/launch/actions/execute_process.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,8 @@ def __init__(
228228
:param: log_cmd if True, prints the final cmd before executing the
229229
process, which is useful for debugging when substitutions are
230230
involved.
231+
:param: cached_output if `True`, both stdout and stderr will be cached.
232+
Use get_stdout() and get_stderr() to read the buffered output.
231233
:param: on_exit list of actions to execute upon process exit.
232234
:param: respawn if 'True', relaunch the process that abnormally died.
233235
Defaults to 'False'.

launch/launch/launch_context.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,17 @@ def would_handle_event(self, event: Event) -> bool:
174174
"""Check whether an event would be handled or not."""
175175
return any(handler.matches(event) for handler in self._event_handlers)
176176

177-
def register_event_handler(self, event_handler: BaseEventHandler) -> None:
178-
"""Register a event handler."""
179-
self._event_handlers.appendleft(event_handler)
177+
def register_event_handler(self, event_handler: BaseEventHandler, append=False) -> None:
178+
"""
179+
Register a event handler.
180+
181+
:param append: if 'true', the new event handler will be executed after the previously
182+
registered ones. If not, it will prepend the old handlers.
183+
"""
184+
if append:
185+
self._event_handlers.append(event_handler)
186+
else:
187+
self._event_handlers.appendleft(event_handler)
180188

181189
def unregister_event_handler(self, event_handler: BaseEventHandler) -> None:
182190
"""Unregister an event handler."""

launch/launch/launch_service.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def __init__(
8686
# it being set to None by run() as it exits.
8787
self.__loop_from_run_thread_lock = threading.RLock()
8888
self.__loop_from_run_thread = None
89+
self.__this_task = None
8990

9091
# Used to indicate when shutdown() has been called.
9192
self.__shutting_down = False
@@ -183,6 +184,7 @@ def _prepare_run_loop(self):
183184
except AttributeError:
184185
this_task = asyncio.Task.current_task(this_loop)
185186

187+
self.__this_task = this_task
186188
# Setup custom signal handlers for SIGINT, SIGTERM and maybe SIGQUIT.
187189
sigint_received = False
188190

@@ -413,3 +415,13 @@ def shutdown(self, force_sync=False) -> Optional[Coroutine]:
413415
def context(self):
414416
"""Getter for context."""
415417
return self.__context
418+
419+
@property
420+
def event_loop(self):
421+
"""Getter for the event loop being used in the thread running the launch service."""
422+
return self.__loop_from_run_thread
423+
424+
@property
425+
def task(self):
426+
"""Return asyncio task associated with this launch service."""
427+
return self.__this_task

0 commit comments

Comments
 (0)