Skip to content
This repository has been archived by the owner on Jan 27, 2020. It is now read-only.

Commit

Permalink
Have IO and Exit Code asserts use the same lookup
Browse files Browse the repository at this point in the history
  - Gave all of these methods the same signature.  They can all take ExecuteProcess actions or strings
  - New util to handle the lookup of process actions in one place
  • Loading branch information
Pete Baughman committed Feb 22, 2019
1 parent 3cdf135 commit eebc4e5
Show file tree
Hide file tree
Showing 12 changed files with 299 additions and 172 deletions.
3 changes: 2 additions & 1 deletion apex_launchtest/apex_launchtest/asserts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
from .assert_exit_codes import EXIT_SIGKILL
from .assert_exit_codes import EXIT_SIGSEGV
from .assert_output import assertInStdout
from .assert_output import NO_CMD_ARGS
from .assert_sequential_output import assertSequentialStdout
from .assert_sequential_output import SequentialTextChecker

from ..util.proc_lookup import NO_CMD_ARGS

__all__ = [
'assertExitCodes',
'assertInStdout',
Expand Down
18 changes: 16 additions & 2 deletions apex_launchtest/apex_launchtest/asserts/assert_exit_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,21 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from ..util import resolveProcesses

EXIT_OK = 0
EXIT_SIGINT = 130
EXIT_SIGQUIT = 131
EXIT_SIGKILL = 137
EXIT_SIGSEGV = 139


def assertExitCodes(proc_info, allowable_exit_codes=[EXIT_OK]):
def assertExitCodes(proc_info,
allowable_exit_codes=[EXIT_OK],
proc=None, # By default, checks all processes
cmd_args=None,
*,
strict_proc_matching=True):
"""
Check the exit codes of the processes under test.
Expand All @@ -30,7 +37,14 @@ def assertExitCodes(proc_info, allowable_exit_codes=[EXIT_OK]):
for code in allowable_exit_codes:
assert isinstance(code, int), "Provided exit code {} is not an int".format(code)

for info in proc_info:
to_check = resolveProcesses(
info_obj=proc_info,
proc=proc,
cmd_args=cmd_args,
strict_proc_matching=strict_proc_matching
)

for info in [proc_info[item] for item in to_check]:
assert info.returncode in allowable_exit_codes, "Proc {} exited with code {}".format(
info.process_name,
info.returncode
Expand Down
116 changes: 16 additions & 100 deletions apex_launchtest/apex_launchtest/asserts/assert_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,89 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import launch.actions

NO_CMD_ARGS = object()


def _proc_to_name_and_args(proc):
# proc is a launch.actions.ExecuteProcess
return "{} {}".format(
proc.process_details['name'],
" ".join(proc.process_details['cmd'][1:])
)


def _assertInStdoutByProcessAction(
proc_output,
msg,
process_action):

for output in proc_output[process_action]:
if msg in output.text.decode('ascii'):
return
else:
assert False, "Did not find '{}' in output for {}".format(
msg,
_proc_to_name_and_args(process_action)
)


def _assertInStdoutByStringProcessName(
proc_output,
msg,
proc_name,
cmd_args,
strict_proc_matching):

# Ensure that the combination proc_name and cmd_args are not ambiguous. If they are,
# we need to cause an error to bubble up in order to alert the test writer that we may not
# be checking what they intend to check

def name_match_fn(proc):
return proc_name in proc.process_details['name']

def cmd_match_fn(proc):
if cmd_args is None:
return True
elif cmd_args is NO_CMD_ARGS:
return len(proc.process_details['cmd']) == 1
else:
return cmd_args in proc.process_details['cmd'][1:]

unique_procs = proc_output.processes()
matching_procs = [proc for proc in unique_procs if name_match_fn(proc) and cmd_match_fn(proc)]

if len(matching_procs) == 0:
proc_names = ', '.join(sorted([_proc_to_name_and_args(proc) for proc in unique_procs]))

raise Exception(
"Did not find any processes matching name '{}' and args '{}'. Procs: {}".format(
proc_name,
cmd_args,
proc_names
)
)
elif strict_proc_matching and len(matching_procs) > 1:
proc_names = ', '.join(sorted([_proc_to_name_and_args(proc) for proc in matching_procs]))
raise Exception(
"Found multiple processes matching name '{}' and cmd_args '{}'. Procs: {}".format(
proc_name,
cmd_args,
proc_names
)
)

for proc in matching_procs: # Nominally just one matching proc
for output in proc_output[proc]:
if msg in output.text.decode('ascii'):
return
else:
assert False, "Did not find '{}' in output for process {} {}".format(
msg,
proc_name,
cmd_args
)
from ..util import resolveProcesses


def assertInStdout(proc_output,
Expand Down Expand Up @@ -126,22 +44,20 @@ def assertInStdout(proc_output,
an error.
:type strict_proc_matching: bool
"""
# Depending on the type of 'proc' we're going to dispatch this a little differently
if isinstance(proc, launch.actions.ExecuteProcess):
_assertInStdoutByProcessAction(
proc_output,
msg,
proc
)
elif isinstance(proc, str):
_assertInStdoutByStringProcessName(
proc_output,
msg,
proc,
cmd_args,
strict_proc_matching
)
resolved_procs = resolveProcesses(
info_obj=proc_output,
proc=proc,
cmd_args=cmd_args,
strict_proc_matching=strict_proc_matching
)

for proc in resolved_procs: # Nominally just one matching proc
for output in proc_output[proc]:
if msg in output.text.decode('ascii'):
return
else:
raise TypeError(
"proc argument must be 'ExecuteProcess' or 'str' not {}".format(type(proc))
names = ', '.join(sorted([p.process_details['name'] for p in resolved_procs]))
assert False, "Did not find '{}' in output for any of the matching process {}".format(
msg,
names
)
31 changes: 21 additions & 10 deletions apex_launchtest/apex_launchtest/asserts/assert_sequential_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

from contextlib import contextmanager

from ..util import resolveProcesses


class SequentialTextChecker:
"""Helper class for asserting that text is found in a certain order."""
Expand Down Expand Up @@ -53,21 +55,30 @@ def assertInStdout(self, msg):

@contextmanager
def assertSequentialStdout(proc_output,
proc):
proc,
cmd_args=None):
"""
Create a context manager used to check stdout occured in a specific order.
:param proc_output: The captured output from a test run
:param proc: The process that generated the output we intend to check
:param proc: The process whose output will be searched
:type proc: A string (search by process name) or a launch.actions.ExecuteProcess object
:param cmd_args: Optional. If 'proc' is a string, cmd_args will be used to disambiguate
processes with the same name. Pass apex_launchtest.asserts.NO_CMD_ARGS to match a proc without
command arguments
:type cmd_args: string
"""
# TODO (pete baughman): Unify this proc lookup [FTR2549]
if isinstance(proc, str):
for process_action in proc_output.processes():
if proc in process_action.process_details['name']:
proc = process_action
break
else:
raise Exception("Did not find process matching name '{}'".format(proc))
proc = resolveProcesses(
proc_output,
proc=proc,
cmd_args=cmd_args,
# There's no good way to sequence output from multiple processes reliably, so we won't
# pretend to be able to. Only allow one matching process for the comination of proc and
# cmd_args
strict_proc_matching=True,
)[0]

# Get all the output from the process. This will be a list of strings. Each string may
# contain multiple lines of output
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Optional
from typing import Text

from launch.actions import ExecuteProcess
from launch.event_handlers import OnProcessIO
from launch.some_actions_type import SomeActionsType

Expand All @@ -25,19 +29,21 @@ class StdoutReadyListener(OnProcessIO):
bit of text
"""

def __init__(self,
proc_name,
ready_txt,
actions: [SomeActionsType]):
self.__proc_name = proc_name
def __init__(
self,
*,
target_action: Optional[ExecuteProcess] = None,
ready_txt: Text,
actions: [SomeActionsType]
):
self.__ready_txt = ready_txt
self.__actions = actions

super().__init__(
target_action=target_action,
on_stdout=self.__on_stdout
)

def __on_stdout(self, process_io):
if self.__proc_name in process_io.process_name:
if self.__ready_txt in process_io.text.decode('ascii'):
return self.__actions
if self.__ready_txt in process_io.text.decode('ascii'):
return self.__actions
48 changes: 36 additions & 12 deletions apex_launchtest/apex_launchtest/io_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import threading

from .asserts.assert_output import assertInStdout
from .util import NoMatchingProcessException
from .util import resolveProcesses


class IoHandler:
Expand Down Expand Up @@ -45,9 +47,11 @@ def processes(self):
:returns [launch.actions.ExecuteProcess]:
"""
return map(
lambda x: x.action,
[self._process_name_dict[name][0] for name in self._process_name_dict]
return list(
map(
lambda x: x.action,
[self._process_name_dict[name][0] for name in self._process_name_dict]
)
)

def process_names(self):
Expand Down Expand Up @@ -122,24 +126,27 @@ def __getitem__(self, key):
with self._sync_lock:
return self._io_handler[key]

def assertWaitFor(self, msg, timeout):
def assertWaitFor(self,
msg,
proc=None, # Will wait for IO from all procs by default
cmd_args=None,
*,
strict_proc_matching=True,
timeout=10):
success = False

def msg_found():
try:
assertInStdout(
self._io_handler, # Use unsynchronized, since this is called from a lock
msg=msg,
proc="", # Will match all process names
cmd_args=None, # Will match all cmd args
strict_proc_matching=False
proc=proc,
cmd_args=cmd_args,
strict_proc_matching=strict_proc_matching
)
return True
except Exception:
# TODO (pete baughman) This is here to handle the case where
# _assertInStdoutByStringProcessName raises an exception because no proc has
# generated output yet. Perhaps a more specific exception should be used and
# then we catch the specific exception type
except NoMatchingProcessException:
# This can happen if no processes have generated any output yet. It's not fatal.
return False
except AssertionError:
return False
Expand All @@ -152,4 +159,21 @@ def msg_found():
msg_found,
timeout=timeout
)

if not success:
# Help the user a little. It's possible that they gave us a bad process name and no
# had no hope of matching anything.
matches = resolveProcesses(self,
proc=proc,
cmd_args=cmd_args,
strict_proc_matching=False)
if len(matches) == 0:
raise Exception(
"After fimeout, found no processes matching '{}' "
"It either doesn't exist, was never launched, "
"or didn't generate any output".format(
proc
)
)

assert success, "Wait for msg '{}' timed out".format(msg)
Loading

0 comments on commit eebc4e5

Please sign in to comment.