Skip to content
Merged
3 changes: 3 additions & 0 deletions openhtf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import openhtf.core.measurements
import openhtf.core.monitors
import openhtf.core.phase_branches
import openhtf.core.phase_child_runner
import openhtf.core.phase_collections
import openhtf.core.phase_descriptor
import openhtf.core.phase_group
Expand Down Expand Up @@ -70,6 +71,7 @@
'DiagnosisCheckpoint',
'DiagnosisCondition',
'PhaseFailureCheckpoint',
'PhaseChildRunner',
'PhaseSequence',
'Subtest',
'PhaseDescriptor',
Expand Down Expand Up @@ -108,6 +110,7 @@
DiagnosisCheckpoint = openhtf.core.phase_branches.DiagnosisCheckpoint
DiagnosisCondition = openhtf.core.phase_branches.DiagnosisCondition
PhaseFailureCheckpoint = openhtf.core.phase_branches.PhaseFailureCheckpoint
ChildRunnerPhase = openhtf.core.phase_child_runner.ChildRunnerPhase

PhaseSequence = openhtf.core.phase_collections.PhaseSequence
Subtest = openhtf.core.phase_collections.Subtest
Expand Down
8 changes: 8 additions & 0 deletions openhtf/core/base_plugs.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def __init__(self, my_config_key)
"""

import logging
import threading
from typing import Any, Dict, Text, Type, Union

import attr
Expand Down Expand Up @@ -139,6 +140,10 @@ def tearDown(self):
# This is overwritten both on the class and the instance so don't store
# a copy of it anywhere.
logger: logging.Logger = _LOG
# Init lock is used to prevent multiple threads from initializing the plug at
# the same time. This is necessary because the method used to pass in the
# the logger is not thread safe.
init_lock = threading.Lock()

@util.classproperty
def placeholder(cls) -> 'PlugPlaceholder': # pylint: disable=no-self-argument
Expand All @@ -164,6 +169,9 @@ def _asdict(self) -> Dict[Text, Any]:
"""
return {}

def setUp(self) -> None:
"""This method is called automatically at the start of each Test execution."""

def tearDown(self) -> None:
"""This method is called automatically at the end of each Test execution."""

Expand Down
40 changes: 40 additions & 0 deletions openhtf/core/phase_child_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import abc
import attr
from typing import Any, Callable, Dict, Text, Type

from openhtf.core import phase_nodes, base_plugs
from openhtf.core import phase_descriptor, base_plugs

@attr.s(slots=True, frozen=True)
class ChildRunnerPhase(phase_nodes.PhaseNode, abc.ABC):
pass

def _asdict(self) -> Dict[Text, Any]:
ret = attr.asdict(self) # pytype: disable=wrong-arg-types # attr-stubs
ret.update(name=self.name, doc=self.doc)
return ret

@property
def name(self) -> Text:
return "child_runner"

def apply_to_all_phases(self, func: Any) -> 'ChildRunnerPhase':
return self

def with_args(self: phase_nodes.WithModifierT,
**kwargs: Any) -> phase_nodes.WithModifierT:
"""Send these keyword-arguments when phases are called."""
del kwargs # Unused.
return self

def with_plugs(
self: phase_nodes.WithModifierT,
**subplugs: Type[base_plugs.BasePlug]) -> phase_nodes.WithModifierT:
"""Substitute plugs for placeholders for this phase, error on unknowns."""
del subplugs # Unused.
return self

def load_code_info(
self: phase_nodes.WithModifierT) -> phase_nodes.WithModifierT:
"""Load coded info for all contained phases."""
return self
13 changes: 13 additions & 0 deletions openhtf/core/results_collector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from typing import List

from openhtf.core import test_record

class ResultsCollector:
def __init__(self):
self._results = []

def on_test_completed(self, test_record: test_record.TestRecord):
self._results.append(test_record)

def get_results(self) -> List[test_record.TestRecord]:
return self._results
101 changes: 58 additions & 43 deletions openhtf/core/test_descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from openhtf.core import test_record as htf_test_record
from openhtf.core import test_state
from openhtf.core.dut_id import DutIdentifier
from openhtf.plugs import PlugManager

from openhtf.util import configuration
from openhtf.util import console_output
Expand All @@ -55,14 +56,14 @@
_LOG = logging.getLogger(__name__)

CONF.declare(
'capture_source',
description=textwrap.dedent(
"""Whether to capture the source of phases and the test module. This
defaults to False since this potentially reads many files and makes large
string copies.
'capture_source',
description=textwrap.dedent(
"""Whether to capture the source of phases and the test module. This
defaults to False since this potentially reads many files and makes large
string copies.

Set to 'true' if you want to capture your test's source."""),
default_value=False)
Set to 'true' if you want to capture your test's source."""),
default_value=False)


class MeasurementNotFoundError(Exception):
Expand Down Expand Up @@ -102,19 +103,19 @@ def create_arg_parser(add_help: bool = False) -> argparse.ArgumentParser:

"""
parser = argparse.ArgumentParser(
'OpenHTF-based testing',
parents=[
CONF.ARG_PARSER,
console_output.ARG_PARSER,
logs.ARG_PARSER,
phase_executor.ARG_PARSER,
],
add_help=add_help)
'OpenHTF-based testing',
parents=[
CONF.ARG_PARSER,
console_output.ARG_PARSER,
logs.ARG_PARSER,
phase_executor.ARG_PARSER,
],
add_help=add_help)
parser.add_argument(
'--config-help',
action='store_true',
help='Instead of executing the test, simply print all available config '
'keys and their description strings.')
'--config-help',
action='store_true',
help='Instead of executing the test, simply print all available config '
'keys and their description strings.')
return parser


Expand All @@ -140,17 +141,23 @@ def PhaseTwo(test):
DEFAULT_SIGINT_HANDLER = None

def __init__(self, *nodes: phase_descriptor.PhaseCallableOrNodeT,
plug_manager: Optional[PlugManager] = None,
child_tests: Optional[List["Test"]] = None,
is_child_test: bool = False,
**metadata: Any):
# Some sanity checks on special metadata keys we automatically fill in.
if 'config' in metadata:
raise KeyError(
'Invalid metadata key "config", it will be automatically populated.')
'Invalid metadata key "config", it will be automatically populated.')

self.created_time_millis = util.time_millis()
self.last_run_time_millis = None
self._test_options = TestOptions()
self._test_options.child_tests = child_tests or []
self._test_options.is_child_test = is_child_test
self._lock = threading.Lock()
self._executor = None
self._plug_manager = plug_manager
# TODO(arsharma): Drop _flatten at some point.
sequence = phase_collections.PhaseSequence(nodes)
self._test_desc = TestDescriptor(sequence,
Expand All @@ -160,9 +167,9 @@ def __init__(self, *nodes: phase_descriptor.PhaseCallableOrNodeT,
if CONF.capture_source:
# Copy the phases with the real CodeInfo for them.
self._test_desc.phase_sequence = (
self._test_desc.phase_sequence.load_code_info())
self._test_desc.phase_sequence.load_code_info())
self._test_desc.code_info = (
htf_test_record.CodeInfo.for_module_from_stack(levels_up=2))
htf_test_record.CodeInfo.for_module_from_stack(levels_up=2))

# Make sure configure() gets called at least once before Execute(). The
# user might call configure() again to override options, but we don't want
Expand Down Expand Up @@ -223,6 +230,10 @@ def state(self) -> Optional[test_state.TestState]:
return self._executor.test_state
return None

@property
def is_child_test(self) -> bool:
return self._test_options.is_child_test

def get_option(self, option: Text) -> Any:
return getattr(self._test_options, option)

Expand Down Expand Up @@ -252,7 +263,8 @@ def configure(self, **kwargs: Any) -> None:
def handle_sig_int(cls, signalnum: Optional[int], handler: Any) -> None:
"""Handle the SIGINT callback."""
if not cls.TEST_INSTANCES:
cls.DEFAULT_SIGINT_HANDLER(signalnum, handler) # pylint: disable=not-callable # pytype: disable=not-callable
cls.DEFAULT_SIGINT_HANDLER(signalnum,
handler) # pylint: disable=not-callable # pytype: disable=not-callable
return

_LOG.error('Received SIGINT, stopping all tests.')
Expand Down Expand Up @@ -296,10 +308,10 @@ def execute(self,
InvalidTestStateError: if this test is already being executed.
"""
phase_descriptor.check_for_duplicate_results(
self._test_desc.phase_sequence.all_phases(),
self._test_options.diagnosers)
self._test_desc.phase_sequence.all_phases(),
self._test_options.diagnosers)
phase_collections.check_for_duplicate_subtest_names(
self._test_desc.phase_sequence)
self._test_desc.phase_sequence)
# Lock this section so we don't .stop() the executor between instantiating
# it and .Start()'ing it, doing so does weird things to the executor state.
with (self._lock):
Expand Down Expand Up @@ -339,7 +351,9 @@ def trigger_phase(test):
self.make_uid(),
trigger,
self._test_options,
run_phases_with_profiling=profile_filename is not None)
run_phases_with_profiling=profile_filename is not None,
plug_manager=self._plug_manager
)

_LOG.info('Executing test: %s', self.descriptor.code_info.name)
self.TEST_INSTANCES[self.uid] = self
Expand Down Expand Up @@ -376,29 +390,28 @@ def trigger_phase(test):
else:
colors = collections.defaultdict(lambda: colorama.Style.BRIGHT)
colors[htf_test_record.Outcome.PASS] = ''.join(
(colorama.Style.BRIGHT, colorama.Fore.GREEN)) # pytype: disable=wrong-arg-types
(colorama.Style.BRIGHT, colorama.Fore.GREEN)) # pytype: disable=wrong-arg-types
colors[htf_test_record.Outcome.FAIL] = ''.join(
(colorama.Style.BRIGHT, colorama.Fore.RED)) # pytype: disable=wrong-arg-types
(colorama.Style.BRIGHT, colorama.Fore.RED)) # pytype: disable=wrong-arg-types
msg_template = (
'test: {name} outcome: {color}{outcome}{marginal}{rst}')
'test: {name} outcome: {color}{outcome}{marginal}{rst}')
console_output.banner_print(
msg_template.format(
name=final_state.test_record.metadata['test_name'],
color=(colorama.Fore.YELLOW
if final_state.test_record.marginal else
colors[final_state.test_record.outcome]),
outcome=final_state.test_record.outcome.name,
marginal=(' (MARGINAL)'
if final_state.test_record.marginal else ''),
rst=colorama.Style.RESET_ALL))
msg_template.format(
name=final_state.test_record.metadata['test_name'],
color=(colorama.Fore.YELLOW
if final_state.test_record.marginal else
colors[final_state.test_record.outcome]),
outcome=final_state.test_record.outcome.name,
marginal=(' (MARGINAL)'
if final_state.test_record.marginal else ''),
rst=colorama.Style.RESET_ALL))
finally:
del self.TEST_INSTANCES[self.uid]
self._executor.close()
self._executor = None

return final_state.test_record.outcome == htf_test_record.Outcome.PASS


@attr.s(slots=True)
class TestOptions(object):
"""Class encapsulating various tunable knobs for Tests and their defaults.
Expand All @@ -419,11 +432,13 @@ class TestOptions(object):

name = attr.ib(type=Text, default='openhtf_test')
output_callbacks = attr.ib(
type=List[Callable[[htf_test_record.TestRecord], None]], factory=list)
type=List[Callable[[htf_test_record.TestRecord], None]], factory=list)
failure_exceptions = attr.ib(type=List[Type[Exception]], factory=list)
default_dut_id = attr.ib(type=Text, default='UNKNOWN_DUT')
stop_on_first_failure = attr.ib(type=bool, default=False)
diagnosers = attr.ib(type=List[diagnoses_lib.BaseTestDiagnoser], factory=list)
is_child_test = attr.ib(type=bool, default=False)
child_tests = attr.ib(type=List[Test], factory=list)


@attr.s(slots=True)
Expand Down Expand Up @@ -573,7 +588,7 @@ def attach_from_file(
IOError: Raised if the given filename couldn't be opened.
"""
self._running_phase_state.attach_from_file(
filename, name=name, mimetype=mimetype)
filename, name=name, mimetype=mimetype)

def get_measurement(
self, measurement_name: Text
Expand Down Expand Up @@ -611,7 +626,7 @@ def get_measurement_strict(
measurement = self._running_test_state.get_measurement(measurement_name)
if measurement is None:
raise MeasurementNotFoundError(
f'Failed to find test measurement {measurement_name}')
f'Failed to find test measurement {measurement_name}')
return measurement

def get_attachment(
Expand Down
Loading
Loading