Skip to content

Commit 4646aa6

Browse files
authored
Handle race condition when a test has errored but is not finalized. (#983)
Add partial test record upload with a background uploader thread. Cleanup Add missing typing.Optional type annotations to function parameters. Fixed parsing of default arguments provided to an OpenHTF phase. Print the OpenHTF result instead of logging it. Replace marginal field in mfg-i facing protos with new MARGINAL_PASS test status. Fix bug causing data to not be uploaded when a ThreadTerminationError occurs. Marginal pass - Propagate marginal determination to the test run level. Fix a few bugs with AllInRange and add unit tests Fix bug with is_marginal check on AllInRange validator Remove circular dependency between diagnoses_lib and phase_descriptor Move check_for_duplicate_results to phase_descriptor Fix and update type annotations in diagnoses_lib Update protos to output marginal determination upstream and add console coloring for marginal output cases. Add marginal measurements. See go/openhtf-marginal-pass. Added useful debugging messages to OpenHTF unit test results when they don't pass. Add a typing.overload to execute_phase_or_test Move openhtf.measures to the phase_descriptor module Added a library to convert OpenHTF objects to strings. Update built in validators to display the spec limits in the database. Fix bug where plugs were being updated twice, resulting in tearDown being called. Update unit test docs to cover TestCase.execute_phase_or_test. Retry setsockopt when starting up the multicast thread Add a decorator-free way to write unit tests. Add capturing of instantiated plugs as an attribute on the test case. Add get_attachment_or_die method to TestApi Regenerate units with the latest UNECE publication (rec20_Rev15e-2020.xls). Raise a clear Error message when a DeviceWrappingPlug is not fully initialized Fix DUT input phase hung w/ ctrl+c (sigint). Timeout when getting multicast.send() responses from queue Add force_repeat option to force a repeat of phase up to the repeat_limit. Adding the phase name to the phase outcome logging statements. Fix type of conf when accessed as openhtf.conf Give 3 retries for timeout phase as default; Add repeat_on_timeout option for phase Replace phase_group member with either phase_sequence or phases when appropriate. Add workaround for when AbortTest plug is not initialized (this happens sometimes, but is not easily reproducible). PiperOrigin-RevId: 381093144
1 parent 70550f9 commit 4646aa6

35 files changed

+2288
-506
lines changed

CONTRIBUTORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ Joe Ethier <[email protected]>
1717
John Hawley <[email protected]>
1818
Keith Suda-Cederquist <[email protected]>
1919
Kenneth Schiller <[email protected]>
20+
Christian Paulin <[email protected]>

examples/all_the_things.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,23 @@ def measures_with_args(test, minimum, maximum):
112112
test.measurements.replaced_min_max = 1
113113

114114

115+
@htf.measures(
116+
htf.Measurement('replaced_marginal_min_only').in_range(
117+
0, 10, '{marginal_minimum}', 8, type=int),
118+
htf.Measurement('replaced_marginal_max_only').in_range(
119+
0, 10, 2, '{marginal_maximum}', type=int),
120+
htf.Measurement('replaced_marginal_min_max').in_range(
121+
0, 10, '{marginal_minimum}', '{marginal_maximum}', type=int),
122+
)
123+
def measures_with_marginal_args(test, marginal_minimum, marginal_maximum):
124+
"""Phase with measurement with marginal arguments."""
125+
del marginal_minimum # Unused.
126+
del marginal_maximum # Unused.
127+
test.measurements.replaced_marginal_min_only = 3
128+
test.measurements.replaced_marginal_max_only = 3
129+
test.measurements.replaced_marginal_min_max = 3
130+
131+
115132
def attachments(test):
116133
test.attach('test_attachment',
117134
'This is test attachment data.'.encode('utf-8'))
@@ -156,6 +173,8 @@ def main():
156173
attachments,
157174
skip_phase,
158175
measures_with_args.with_args(minimum=1, maximum=4),
176+
measures_with_marginal_args.with_args(
177+
marginal_minimum=4, marginal_maximum=6),
159178
analysis,
160179
),
161180
# Some metadata fields, these in particular are used by mfg-inspector,

examples/measurements.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,18 @@ def multdim_measurements(test):
162162
test.measurements['average_current'])
163163

164164

165+
# Marginal measurements can be used to obtain a finer granularity of by how much
166+
# a measurement is passing. Marginal measurements have stricter minimum and
167+
# maximum limits, which are used to flag measurements/phase/test records as
168+
# marginal without affecting the overall phase outcome.
169+
@htf.measures(
170+
htf.Measurement('resistance').with_units('ohm').in_range(
171+
minimum=5, maximum=17, marginal_minimum=9, marginal_maximum=11))
172+
def marginal_measurements(test):
173+
"""Phase with a marginal measurement."""
174+
test.measurements.resistance = 13
175+
176+
165177
def main():
166178
# We instantiate our OpenHTF test with the phases we want to run as args.
167179
test = htf.Test(hello_phase, again_phase, lots_of_measurements,

openhtf/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414
"""The main OpenHTF entry point."""
1515

1616
import signal
17+
import typing
1718

1819
from openhtf import plugs
1920
from openhtf.core import phase_executor
2021
from openhtf.core import test_record
2122
from openhtf.core.base_plugs import BasePlug
22-
from openhtf.core.diagnoses_lib import diagnose
2323
from openhtf.core.diagnoses_lib import DiagnosesStore
2424
from openhtf.core.diagnoses_lib import Diagnosis
2525
from openhtf.core.diagnoses_lib import DiagnosisComponent
@@ -30,14 +30,15 @@
3030

3131
from openhtf.core.measurements import Dimension
3232
from openhtf.core.measurements import Measurement
33-
from openhtf.core.measurements import measures
3433
from openhtf.core.monitors import monitors
3534
from openhtf.core.phase_branches import BranchSequence
3635
from openhtf.core.phase_branches import DiagnosisCheckpoint
3736
from openhtf.core.phase_branches import DiagnosisCondition
3837
from openhtf.core.phase_branches import PhaseFailureCheckpoint
3938
from openhtf.core.phase_collections import PhaseSequence
4039
from openhtf.core.phase_collections import Subtest
40+
from openhtf.core.phase_descriptor import diagnose
41+
from openhtf.core.phase_descriptor import measures
4142
from openhtf.core.phase_descriptor import PhaseDescriptor
4243
from openhtf.core.phase_descriptor import PhaseOptions
4344
from openhtf.core.phase_descriptor import PhaseResult
@@ -57,6 +58,9 @@
5758
from openhtf.util import units
5859
import pkg_resources
5960

61+
if typing.TYPE_CHECKING:
62+
conf: conf.Configuration # Configuration is only available here in typing.
63+
6064

6165
def get_version():
6266
"""Returns the version string of the 'openhtf' package.

openhtf/core/diagnoses_lib.py

Lines changed: 2 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,11 @@ def main():
123123
"""
124124

125125
import abc
126-
import collections
127126
import logging
128-
from typing import Any, Callable, DefaultDict, Dict, Iterable, Iterator, List, Optional, Sequence, Set, Text, Type, TYPE_CHECKING, Union
127+
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Text, Type, TYPE_CHECKING, Union
129128

130129
import attr
131130
import enum # pylint: disable=g-bad-import-order
132-
from openhtf.core import phase_descriptor
133131
from openhtf.core import test_record
134132
from openhtf.util import data
135133
import six
@@ -147,10 +145,6 @@ class InvalidDiagnosisError(Exception):
147145
"""A Diagnosis was constructed incorrectly."""
148146

149147

150-
class DuplicateResultError(Exception):
151-
"""Different DiagResultEnum instances define the same value."""
152-
153-
154148
@attr.s(slots=True)
155149
class DiagnosesStore(object):
156150
"""Storage and lookup of diagnoses."""
@@ -265,42 +259,6 @@ def execute_test_diagnoser(self, diagnoser: 'BaseTestDiagnoser',
265259
self._add_diagnosis(diag)
266260

267261

268-
def check_for_duplicate_results(
269-
phase_iterator: Iterator[phase_descriptor.PhaseDescriptor],
270-
test_diagnosers: Sequence['BaseTestDiagnoser']) -> None:
271-
"""Check for any results with the same enum value in different ResultTypes.
272-
273-
Args:
274-
phase_iterator: iterator over the phases to check.
275-
test_diagnosers: list of test level diagnosers.
276-
277-
Raises:
278-
DuplicateResultError: when duplicate enum values are found.
279-
"""
280-
all_result_enums = set() # type: Set[Type['DiagResultEnum']]
281-
for phase in phase_iterator:
282-
for phase_diag in phase.diagnosers:
283-
all_result_enums.add(phase_diag.result_type)
284-
for test_diag in test_diagnosers:
285-
all_result_enums.add(test_diag.result_type)
286-
287-
values_to_enums = collections.defaultdict(
288-
list) # type: DefaultDict[str, Type['DiagResultEnum']]
289-
for enum_cls in all_result_enums:
290-
for entry in enum_cls:
291-
values_to_enums[entry.value].append(enum_cls)
292-
293-
duplicates = [] # type: List[str]
294-
for result_value, enum_classes in sorted(values_to_enums.items()):
295-
if len(enum_classes) > 1:
296-
duplicates.append('Value "{}" defined by {}'.format(
297-
result_value, enum_classes))
298-
if not duplicates:
299-
return
300-
raise DuplicateResultError('Duplicate DiagResultEnum values: {}'.format(
301-
'\n'.join(duplicates)))
302-
303-
304262
def _check_diagnoser(diagnoser: '_BaseDiagnoser',
305263
diagnoser_cls: Type['_BaseDiagnoser']) -> None:
306264
"""Check that a diagnoser is properly created."""
@@ -377,8 +335,7 @@ class BasePhaseDiagnoser(six.with_metaclass(abc.ABCMeta, _BaseDiagnoser)):
377335
__slots__ = ()
378336

379337
@abc.abstractmethod
380-
def run(self,
381-
phase_record: phase_descriptor.PhaseDescriptor) -> DiagnoserReturnT:
338+
def run(self, phase_record: test_record.PhaseRecord) -> DiagnoserReturnT:
382339
"""Must be implemented to return list of Diagnoses instances.
383340
384341
Args:
@@ -547,21 +504,3 @@ def __attrs_post_init__(self) -> None:
547504
def as_base_types(self) -> Dict[Text, Any]:
548505
return data.convert_to_base_types(
549506
attr.asdict(self, filter=_diagnosis_serialize_filter))
550-
551-
552-
def diagnose(
553-
*diagnosers: BasePhaseDiagnoser
554-
) -> Callable[[phase_descriptor.PhaseT], phase_descriptor.PhaseDescriptor]:
555-
"""Decorator to add diagnosers to a PhaseDescriptor."""
556-
check_diagnosers(diagnosers, BasePhaseDiagnoser)
557-
diags = list(diagnosers)
558-
559-
def decorate(
560-
wrapped_phase: phase_descriptor.PhaseT
561-
) -> phase_descriptor.PhaseDescriptor:
562-
"""Phase decorator to be returned."""
563-
phase = phase_descriptor.PhaseDescriptor.wrap_or_copy(wrapped_phase)
564-
phase.diagnosers.extend(diags)
565-
return phase
566-
567-
return decorate

openhtf/core/measurements.py

Lines changed: 19 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
Measurements are described by the measurements.Measurement class. Essentially,
2323
the Measurement class is used by test authors to declare measurements by name,
2424
and to optionally provide unit, type, and validation information. Measurements
25-
are attached to Test Phases using the @measurements.measures() decorator.
25+
are attached to Test Phases using the @openhtf.measures() decorator.
2626
2727
When measurements are output by the OpenHTF framework, the Measurement objects
2828
are serialized into the 'measurements' field on the PhaseRecord, which contain
@@ -45,11 +45,11 @@
4545
4646
Examples:
4747
48-
@measurements.measures(
48+
@openhtf.measures(
4949
measurements.Measurement(
5050
'number_widgets').in_range(5, 10).doc(
5151
'''This phase parameter tracks the number of widgets.'''))
52-
@measurements.measures(
52+
@openhtf.measures(
5353
*(measurements.Measurement('level_%s' % lvl)
5454
for lvl in ('none', 'some', 'all')))
5555
def WidgetTestPhase(test):
@@ -62,17 +62,18 @@ def WidgetTestPhase(test):
6262
import enum
6363
import functools
6464
import logging
65+
import typing
6566
from typing import Any, Callable, Dict, Iterator, List, Optional, Text, Tuple, Union
6667

6768
import attr
6869

6970
from openhtf import util
70-
from openhtf.core import diagnoses_lib
71-
from openhtf.core import phase_descriptor
7271
from openhtf.util import data
7372
from openhtf.util import units as util_units
7473
from openhtf.util import validators
7574
import six
75+
if typing.TYPE_CHECKING:
76+
from openhtf.core import diagnoses_lib
7677

7778
try:
7879
# pylint: disable=g-import-not-at-top
@@ -119,7 +120,7 @@ class _ConditionalValidator(object):
119120
"""Conditional validator declaration."""
120121

121122
# The diagnosis result required for the validator to be used.
122-
result = attr.ib(type=diagnoses_lib.DiagResultEnum)
123+
result = attr.ib(type='diagnoses_lib.DiagResultEnum')
123124

124125
# The validator to use when the result is present.
125126
validator = attr.ib(type=Callable[[Any], bool])
@@ -187,6 +188,8 @@ class Measurement(object):
187188
notification_cb: An optional function to be called when the measurement is
188189
set.
189190
outcome: One of the Outcome() enumeration values, starting at UNSET.
191+
marginal: A bool flag indicating if this measurement is marginal if the
192+
outcome is PASS.
190193
_cached: A cached dict representation of this measurement created initially
191194
during as_base_types and updated in place to save allocation time.
192195
"""
@@ -211,6 +214,7 @@ class Measurement(object):
211214
type=Union['MeasuredValue', 'DimensionedMeasuredValue'], default=None)
212215
_notification_cb = attr.ib(type=Optional[Callable[[], None]], default=None)
213216
outcome = attr.ib(type=Outcome, default=Outcome.UNSET)
217+
marginal = attr.ib(type=bool, default=False)
214218

215219
# Runtime cache to speed up conversions.
216220
_cached = attr.ib(type=Optional[Dict[Text, Any]], default=None)
@@ -341,7 +345,7 @@ def with_validator(self, validator: Callable[[Any], bool]) -> 'Measurement':
341345
return self
342346

343347
def validate_on(
344-
self, result_to_validator_mapping: Dict[diagnoses_lib.DiagResultEnum,
348+
self, result_to_validator_mapping: Dict['diagnoses_lib.DiagResultEnum',
345349
Callable[[Any], bool]]
346350
) -> 'Measurement':
347351
"""Adds conditional validators.
@@ -414,11 +418,17 @@ def _with_validator(*args, **kwargs):
414418
return _with_validator
415419

416420
def validate(self) -> 'Measurement':
417-
"""Validate this measurement and update its 'outcome' field."""
421+
"""Validate this measurement and update 'outcome' and 'marginal' fields."""
418422
# PASS if all our validators return True, otherwise FAIL.
419423
try:
420424
if all(v(self._measured_value.value) for v in self.validators):
421425
self.outcome = Outcome.PASS
426+
427+
# Only check marginality for passing measurements.
428+
if any(
429+
hasattr(v, 'is_marginal') and
430+
v.is_marginal(self._measured_value.value) for v in self.validators):
431+
self.marginal = True
422432
else:
423433
self.outcome = Outcome.FAIL
424434
return self
@@ -811,69 +821,9 @@ def __getitem__(self, name: Text) -> Any:
811821
# Return the MeasuredValue's value, MeasuredValue will raise if not set.
812822
return m.measured_value.value
813823

824+
814825
# Work around for attrs bug in 20.1.0; after the next release, this can be
815826
# removed and `Collection._custom_setattr` can be renamed to `__setattr__`.
816827
# https://github.com/python-attrs/attrs/issues/680
817828
Collection.__setattr__ = Collection._custom_setattr # pylint: disable=protected-access
818829
del Collection._custom_setattr
819-
820-
821-
def measures(
822-
*measurements: Union[Text, Measurement], **kwargs: Any
823-
) -> Callable[[phase_descriptor.PhaseT], phase_descriptor.PhaseDescriptor]:
824-
"""Decorator-maker used to declare measurements for phases.
825-
826-
See the measurements module docstring for examples of usage.
827-
828-
Args:
829-
*measurements: Measurement objects to declare, or a string name from which
830-
to create a Measurement.
831-
**kwargs: Keyword arguments to pass to Measurement constructor if we're
832-
constructing one. Note that if kwargs are provided, the length of
833-
measurements must be 1, and that value must be a string containing the
834-
measurement name. For valid kwargs, see the definition of the Measurement
835-
class.
836-
837-
Raises:
838-
InvalidMeasurementTypeError: When the measurement is not defined correctly.
839-
840-
Returns:
841-
A decorator that declares the measurement(s) for the decorated phase.
842-
"""
843-
844-
def _maybe_make(meas: Union[Text, Measurement]) -> Measurement:
845-
"""Turn strings into Measurement objects if necessary."""
846-
if isinstance(meas, Measurement):
847-
return meas
848-
elif isinstance(meas, six.string_types):
849-
return Measurement(meas, **kwargs)
850-
raise InvalidMeasurementTypeError('Expected Measurement or string', meas)
851-
852-
# In case we're declaring a measurement inline, we can only declare one.
853-
if kwargs and len(measurements) != 1:
854-
raise InvalidMeasurementTypeError(
855-
'If @measures kwargs are provided, a single measurement name must be '
856-
'provided as a positional arg first.')
857-
858-
# Unlikely, but let's make sure we don't allow overriding initial outcome.
859-
if 'outcome' in kwargs:
860-
raise ValueError('Cannot specify outcome in measurement declaration!')
861-
862-
measurements = [_maybe_make(meas) for meas in measurements]
863-
864-
# 'measurements' is guaranteed to be a list of Measurement objects here.
865-
def decorate(
866-
wrapped_phase: phase_descriptor.PhaseT
867-
) -> phase_descriptor.PhaseDescriptor:
868-
"""Phase decorator to be returned."""
869-
phase = phase_descriptor.PhaseDescriptor.wrap_or_copy(wrapped_phase)
870-
duplicate_names = (
871-
set(m.name for m in measurements)
872-
& set(m.name for m in phase.measurements))
873-
if duplicate_names:
874-
raise DuplicateNameError('Measurement names duplicated', duplicate_names)
875-
876-
phase.measurements.extend(measurements)
877-
return phase
878-
879-
return decorate

openhtf/core/monitors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def wrapper(
155155

156156
@openhtf.PhaseOptions(requires_state=True)
157157
@plugs.plug(update_kwargs=False, **monitor_plugs)
158-
@measurements.measures(
158+
@openhtf.measures(
159159
measurements.Measurement(measurement_name).with_units(
160160
units).with_dimensions(uom.MILLISECOND))
161161
@functools.wraps(phase_desc.func)

0 commit comments

Comments
 (0)