Skip to content

Commit fe137e6

Browse files
committed
Add child runner phase
1 parent e56be6c commit fe137e6

File tree

5 files changed

+102
-4
lines changed

5 files changed

+102
-4
lines changed

openhtf/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import openhtf.core.measurements
2626
import openhtf.core.monitors
2727
import openhtf.core.phase_branches
28+
import openhtf.core.phase_child_runner
2829
import openhtf.core.phase_collections
2930
import openhtf.core.phase_descriptor
3031
import openhtf.core.phase_group
@@ -70,6 +71,7 @@
7071
'DiagnosisCheckpoint',
7172
'DiagnosisCondition',
7273
'PhaseFailureCheckpoint',
74+
'PhaseChildRunner',
7375
'PhaseSequence',
7476
'Subtest',
7577
'PhaseDescriptor',
@@ -108,6 +110,7 @@
108110
DiagnosisCheckpoint = openhtf.core.phase_branches.DiagnosisCheckpoint
109111
DiagnosisCondition = openhtf.core.phase_branches.DiagnosisCondition
110112
PhaseFailureCheckpoint = openhtf.core.phase_branches.PhaseFailureCheckpoint
113+
ChildRunnerPhase = openhtf.core.phase_child_runner.ChildRunnerPhase
111114

112115
PhaseSequence = openhtf.core.phase_collections.PhaseSequence
113116
Subtest = openhtf.core.phase_collections.Subtest

openhtf/core/phase_child_runner.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import abc
2+
import attr
3+
from typing import Any, Callable, Dict, Text, Type
4+
5+
from openhtf.core import phase_nodes, base_plugs
6+
from openhtf.core import phase_descriptor, base_plugs
7+
8+
@attr.s(slots=True, frozen=True)
9+
class ChildRunnerPhase(phase_nodes.PhaseNode, abc.ABC):
10+
pass
11+
12+
def _asdict(self) -> Dict[Text, Any]:
13+
ret = attr.asdict(self) # pytype: disable=wrong-arg-types # attr-stubs
14+
ret.update(name=self.name, doc=self.doc)
15+
return ret
16+
17+
@property
18+
def name(self) -> Text:
19+
return "child_runner"
20+
21+
def apply_to_all_phases(self, func: Any) -> 'ChildRunnerPhase':
22+
return self
23+
24+
def with_args(self: phase_nodes.WithModifierT,
25+
**kwargs: Any) -> phase_nodes.WithModifierT:
26+
"""Send these keyword-arguments when phases are called."""
27+
del kwargs # Unused.
28+
return self
29+
30+
def with_plugs(
31+
self: phase_nodes.WithModifierT,
32+
**subplugs: Type[base_plugs.BasePlug]) -> phase_nodes.WithModifierT:
33+
"""Substitute plugs for placeholders for this phase, error on unknowns."""
34+
del subplugs # Unused.
35+
return self
36+
37+
def load_code_info(
38+
self: phase_nodes.WithModifierT) -> phase_nodes.WithModifierT:
39+
"""Load coded info for all contained phases."""
40+
return self

openhtf/core/test_descriptor.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ def PhaseTwo(test):
142142

143143
def __init__(self, *nodes: phase_descriptor.PhaseCallableOrNodeT,
144144
plug_manager: Optional[PlugManager] = None,
145+
child_tests: Optional[List["Test"]] = None,
146+
is_child_test: bool = False,
145147
**metadata: Any):
146148
# Some sanity checks on special metadata keys we automatically fill in.
147149
if 'config' in metadata:
@@ -151,6 +153,8 @@ def __init__(self, *nodes: phase_descriptor.PhaseCallableOrNodeT,
151153
self.created_time_millis = util.time_millis()
152154
self.last_run_time_millis = None
153155
self._test_options = TestOptions()
156+
self._test_options.child_tests = child_tests or []
157+
self._test_options.is_child_test = is_child_test
154158
self._lock = threading.Lock()
155159
self._executor = None
156160
self._plug_manager = plug_manager
@@ -226,6 +230,10 @@ def state(self) -> Optional[test_state.TestState]:
226230
return self._executor.test_state
227231
return None
228232

233+
@property
234+
def is_child_test(self) -> bool:
235+
return self._test_options.is_child_test
236+
229237
def get_option(self, option: Text) -> Any:
230238
return getattr(self._test_options, option)
231239

@@ -404,7 +412,6 @@ def trigger_phase(test):
404412

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

407-
408415
@attr.s(slots=True)
409416
class TestOptions(object):
410417
"""Class encapsulating various tunable knobs for Tests and their defaults.
@@ -430,6 +437,8 @@ class TestOptions(object):
430437
default_dut_id = attr.ib(type=Text, default='UNKNOWN_DUT')
431438
stop_on_first_failure = attr.ib(type=bool, default=False)
432439
diagnosers = attr.ib(type=List[diagnoses_lib.BaseTestDiagnoser], factory=list)
440+
is_child_test = attr.ib(type=bool, default=False)
441+
child_tests = attr.ib(type=List[Test], factory=list)
433442

434443

435444
@attr.s(slots=True)

openhtf/core/test_executor.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,24 @@
2020
import sys
2121
import tempfile
2222
import threading
23+
import time
2324
import traceback
2425
from typing import Iterator, List, Optional, Text, Type, TYPE_CHECKING
2526

2627
from openhtf import util
2728
from openhtf.core import base_plugs
2829
from openhtf.core import diagnoses_lib
2930
from openhtf.core import phase_branches
31+
from openhtf.core import phase_child_runner
3032
from openhtf.core import phase_collections
3133
from openhtf.core import phase_descriptor
3234
from openhtf.core import phase_executor
3335
from openhtf.core import phase_group
3436
from openhtf.core import phase_nodes
37+
from openhtf.core import test_descriptor
3538
from openhtf.core import test_record
3639
from openhtf.core import test_state
40+
from openhtf.core.results_collector import ResultsCollector
3741
from openhtf.plugs import PlugManager
3842
from openhtf.util import configuration
3943
from openhtf.util import threads
@@ -383,11 +387,48 @@ def _execute_checkpoint(self, checkpoint: phase_branches.Checkpoint,
383387
self.phase_executor.skip_checkpoint(checkpoint, subtest_rec)
384388
return _ExecutorReturn.CONTINUE
385389

386-
outcome = self.phase_executor.evaluate_checkpoint(checkpoint, subtest_rec)
390+
def _execute_children(self, child_runner: phase_child_runner.ChildRunnerPhase,
391+
subtest_rec: Optional[test_record.SubtestRecord],
392+
in_teardown: bool) -> _ExecutorReturn:
393+
394+
tests = self._test_options.child_tests # type: list[test_descriptor.Test]
395+
if len(tests) == 0:
396+
self.logger.warning("There are no child tests to execute.")
397+
return _ExecutorReturn.CONTINUE
398+
399+
results = ResultsCollector()
400+
401+
def test_runner(child_test: test_descriptor.Test):
402+
res = child_test.execute()
403+
self.logger.debug(f"test uid {child_test.uid} finished, with result: {res}")
404+
405+
execution_threads = []
406+
for test in tests:
407+
test.add_output_callbacks(results.on_test_completed)
408+
thread = threading.Thread(target=test_runner, args=(test,))
409+
execution_threads.append(thread)
410+
thread.start()
411+
412+
self.logger.debug('Waiting for all tests to complete.')
413+
while any([not t.state.is_finalized if t.state is not None else False for t in tests]):
414+
time.sleep(0.1)
415+
416+
for thread in execution_threads:
417+
thread.join()
418+
419+
self.logger.debug('All child tests complete.')
420+
421+
outcome = phase_executor.PhaseExecutionOutcome(phase_descriptor.PhaseResult.CONTINUE)
422+
423+
if any(res.outcome != test_record.Outcome.PASS for res in results.get_results()):
424+
if self._test_options.stop_on_first_failure:
425+
outcome = phase_executor.PhaseExecutionOutcome(
426+
phase_descriptor.PhaseResult.STOP)
427+
387428
if outcome.is_terminal:
388429
if not self._last_outcome:
389430
self._last_outcome = outcome
390-
self._last_execution_unit = checkpoint.name
431+
self._last_execution_unit = child_runner.name
391432
return _ExecutorReturn.TERMINAL
392433

393434
if outcome.is_fail_subtest:
@@ -616,6 +657,8 @@ def _execute_node(self, node: phase_nodes.PhaseNode,
616657
return self._execute_phase(node, subtest_rec, in_teardown)
617658
if isinstance(node, phase_branches.Checkpoint):
618659
return self._execute_checkpoint(node, subtest_rec, in_teardown)
660+
if isinstance(node, phase_child_runner.ChildRunnerPhase):
661+
return self._execute_children(node, subtest_rec, in_teardown)
619662
self.logger.error('Unhandled node type: %s', node)
620663
return _ExecutorReturn.TERMINAL
621664

@@ -639,4 +682,4 @@ def _execute_test_diagnoser(
639682

640683
def _execute_test_diagnosers(self) -> None:
641684
for diagnoser in self._test_options.diagnosers:
642-
self._execute_test_diagnoser(diagnoser)
685+
self._execute_test_diagnoser(diagnoser)

openhtf/output/servers/station_server.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ def _get_executing_test():
9292
if not tests:
9393
return None, None
9494

95+
# Filter out child tests
96+
tests = [test for test in tests if not test.is_child_test]
97+
9598
if len(tests) > 1:
9699
_LOG.warning('Station server does not support multiple executing tests.')
97100

0 commit comments

Comments
 (0)