Skip to content

Commit b3a56ee

Browse files
Merge pull request #1230 from glados-verma:feat-test-measurements-and-helper
PiperOrigin-RevId: 761656356
2 parents 3f25449 + 249ae91 commit b3a56ee

File tree

6 files changed

+241
-72
lines changed

6 files changed

+241
-72
lines changed

examples/hello_world.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@
2626
For more information on output, see the output.py example.
2727
"""
2828

29+
import os.path
30+
2931
# Import openhtf with an abbreviated name, as we'll be using a bunch of stuff
3032
# from it throughout our test scripts. See __all__ at the top of
3133
# openhtf/__init__.py for details on what's in top-of-module namespace.
3234
import openhtf as htf
33-
3435
# Import this output mechanism as it's the specific one we want to use.
3536
from openhtf.output.callbacks import json_factory
36-
3737
from openhtf.plugs import user_input
3838

3939

@@ -63,7 +63,7 @@ def hello_world(test):
6363
test.measurements.hello_world_measurement = 'Hello Again!'
6464

6565

66-
def main():
66+
def create_and_run_test(output_dir: str = '.'):
6767
# We instantiate our OpenHTF test with the phases we want to run as args.
6868
# Multiple phases would be passed as additional args, and additional
6969
# keyword arguments may be passed as well. See other examples for more
@@ -73,10 +73,13 @@ def main():
7373
# In order to view the result of the test, we have to output it somewhere,
7474
# and a local JSON file is a convenient way to do this. Custom output
7575
# mechanisms can be implemented, but for now we'll just keep it simple.
76-
# This will always output to the same ./hello_world.json file, formatted
77-
# slightly for human readability.
76+
# With the default output_dir argument, this will always output to the
77+
# same ./hello_world.json file, formatted slightly for human readability.
7878
test.add_output_callbacks(
79-
json_factory.OutputToJSON('./{dut_id}.hello_world.json', indent=2))
79+
json_factory.OutputToJSON(
80+
os.path.join(output_dir, '{dut_id}.hello_world.json'), indent=2
81+
)
82+
)
8083

8184
# prompt_for_test_start prompts the operator for a DUT ID, a unique identifier
8285
# for the DUT (Device Under Test). OpenHTF requires that a DUT ID is set
@@ -89,4 +92,4 @@ def main():
8992

9093

9194
if __name__ == '__main__':
92-
main()
95+
create_and_run_test()

examples/measurements.py

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,15 @@
4343
measurements, which some output formats require.
4444
"""
4545

46+
import os.path
47+
import random
48+
4649
# Import openhtf with an abbreviated name, as we'll be using a bunch of stuff
4750
# from it throughout our test scripts. See __all__ at the top of
4851
# openhtf/__init__.py for details on what's in top-of-module namespace.
49-
import random
50-
5152
import openhtf as htf
52-
5353
# Import this output mechanism as it's the specific one we want to use.
5454
from openhtf.output.callbacks import json_factory
55-
5655
# You won't normally need to import this, see validators.py example for
5756
# more details. It's used for the inline measurement declaration example
5857
# below, but normally you'll only import it when you want to define custom
@@ -94,9 +93,11 @@ def lots_of_measurements(test):
9493
# describing the measurement. Validators can get quite complex, for more
9594
# details, see the validators.py example.
9695
@htf.measures(
97-
htf.Measurement('validated_measurement').in_range(
98-
0,
99-
10).doc('This measurement is validated.').with_units(htf.units.SECOND))
96+
htf.Measurement('validated_measurement')
97+
.in_range(0, 10)
98+
.doc('This measurement is validated.')
99+
.with_units(htf.units.SECOND)
100+
)
100101
def measure_seconds(test):
101102
# The 'outcome' of this measurement in the test_record result will be a PASS
102103
# because its value passes the validator specified (0 <= 5 <= 10).
@@ -112,7 +113,8 @@ def measure_seconds(test):
112113
'inline_kwargs',
113114
docstring='This measurement is declared inline!',
114115
units=htf.units.HERTZ,
115-
validators=[validators.in_range(0, 10)])
116+
validators=[validators.in_range(0, 10)],
117+
)
116118
@htf.measures('another_inline', docstring='Because why not?')
117119
def inline_phase(test):
118120
"""Phase that declares a measurements validators as a keyword argument."""
@@ -128,7 +130,8 @@ def inline_phase(test):
128130
# A multidim measurement including how to convert to a pandas dataframe and
129131
# a numpy array.
130132
@htf.measures(
131-
htf.Measurement('power_time_series').with_dimensions('ms', 'V', 'A'))
133+
htf.Measurement('power_time_series').with_dimensions('ms', 'V', 'A')
134+
)
132135
@htf.measures(htf.Measurement('average_voltage').with_units('V'))
133136
@htf.measures(htf.Measurement('average_current').with_units('A'))
134137
@htf.measures(htf.Measurement('resistance').with_units('ohm').in_range(9, 11))
@@ -138,7 +141,7 @@ def multdim_measurements(test):
138141
for t in range(10):
139142
resistance = 10
140143
voltage = 10 + 10.0 * t
141-
current = voltage / resistance + .01 * random.random()
144+
current = voltage / resistance + 0.01 * random.random()
142145
dimensions = (t, voltage, current)
143146
test.measurements['power_time_series'][dimensions] = 0
144147

@@ -158,39 +161,51 @@ def multdim_measurements(test):
158161

159162
# Finally, let's estimate the resistance
160163
test.measurements['resistance'] = (
161-
test.measurements['average_voltage'] /
162-
test.measurements['average_current'])
164+
test.measurements['average_voltage']
165+
/ test.measurements['average_current']
166+
)
163167

164168

165169
# Marginal measurements can be used to obtain a finer granularity of by how much
166170
# a measurement is passing. Marginal measurements have stricter minimum and
167171
# maximum limits, which are used to flag measurements/phase/test records as
168172
# marginal without affecting the overall phase outcome.
169173
@htf.measures(
170-
htf.Measurement('resistance').with_units('ohm').in_range(
171-
minimum=5, maximum=17, marginal_minimum=9, marginal_maximum=11))
174+
htf.Measurement('resistance')
175+
.with_units('ohm')
176+
.in_range(minimum=5, maximum=17, marginal_minimum=9, marginal_maximum=11)
177+
)
172178
def marginal_measurements(test):
173179
"""Phase with a marginal measurement."""
174180
test.measurements.resistance = 13
175181

176182

177-
def main():
183+
def create_and_run_test(output_dir: str = '.'):
178184
# We instantiate our OpenHTF test with the phases we want to run as args.
179-
test = htf.Test(hello_phase, again_phase, lots_of_measurements,
180-
measure_seconds, inline_phase, multdim_measurements)
185+
test = htf.Test(
186+
hello_phase,
187+
again_phase,
188+
lots_of_measurements,
189+
measure_seconds,
190+
inline_phase,
191+
multdim_measurements,
192+
)
181193

182194
# In order to view the result of the test, we have to output it somewhere,
183195
# and a local JSON file is a convenient way to do this. Custom output
184196
# mechanisms can be implemented, but for now we'll just keep it simple.
185197
# This will always output to the same ./measurements.json file, formatted
186198
# slightly for human readability.
187199
test.add_output_callbacks(
188-
json_factory.OutputToJSON('./measurements.json', indent=2))
200+
json_factory.OutputToJSON(
201+
os.path.join(output_dir, 'measurements.json'), indent=2
202+
)
203+
)
189204

190205
# Unlike hello_world.py, where we prompt for a DUT ID, here we'll just
191206
# use an arbitrary one.
192207
test.execute(test_start=lambda: 'MyDutId')
193208

194209

195210
if __name__ == '__main__':
196-
main()
211+
create_and_run_test()

openhtf/util/example_test.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright 2016 Google Inc. All Rights Reserved.
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Utilities for integration testing OpenHTF examples."""
15+
16+
from typing import Any
17+
import unittest
18+
19+
20+
class ExampleTestBase(unittest.TestCase):
21+
"""Base class for integration testing OpenHTF examples."""
22+
23+
def get_phase_by_name(
24+
self, json_data: dict[str, Any], phase_name: str
25+
) -> dict[str, Any]:
26+
"""Finds a phase by its name in a test output JSON.
27+
28+
Args:
29+
json_data: The JSON data as loaded from the output file, generated by the
30+
default JSON output callback.
31+
phase_name: The name of the phase to find.
32+
33+
Returns:
34+
The phase dictionary if found.
35+
36+
Raises:
37+
AssertionError (via test_case.fail()): If the phase is not found
38+
or json_data does not have the expected "phases" key.
39+
"""
40+
if "phases" not in json_data:
41+
self.fail("JSON data does not contain 'phases' key.")
42+
for p in json_data["phases"]:
43+
if p["name"] == phase_name:
44+
return p
45+
self.fail(f"Phase '{phase_name}' not found in output.")

test/examples/hello_world_test.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Copyright 2016 Google Inc. All Rights Reserved.
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import json
16+
import os
17+
import tempfile
18+
import unittest
19+
from unittest import mock
20+
21+
from examples import hello_world
22+
from openhtf.plugs import user_input
23+
from openhtf.util import example_test
24+
25+
26+
class TestHelloWorld(example_test.ExampleTestBase):
27+
28+
@mock.patch.object(user_input.UserInput, user_input.UserInput.prompt.__name__)
29+
def test_main_execution(self, mock_user_prompt):
30+
# The test only has one prompt, for the DUT ID.
31+
mock_user_prompt.return_value = "test_dut"
32+
33+
with tempfile.TemporaryDirectory() as temp_dir:
34+
hello_world.create_and_run_test(temp_dir)
35+
expected_json_path = os.path.join(temp_dir, 'test_dut.hello_world.json')
36+
37+
# Assert that the output file was created
38+
self.assertTrue(os.path.exists(expected_json_path))
39+
with open(expected_json_path) as f:
40+
output_data = json.load(f)
41+
42+
self.assertEqual(output_data["dut_id"], "test_dut")
43+
self.assertEqual(output_data["outcome"], "PASS")
44+
self.assertGreaterEqual(len(output_data["phases"]), 2)
45+
46+
hello_world_phase_data = self.get_phase_by_name(output_data, "hello_world")
47+
self.assertEqual(
48+
hello_world_phase_data["measurements"]["hello_world_measurement"][
49+
"measured_value"
50+
],
51+
"Hello Again!",
52+
)
53+
54+
55+
if __name__ == "__main__":
56+
unittest.main()

test/examples/measurements_test.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Copyright 2016 Google Inc. All Rights Reserved.
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import json
16+
import os
17+
import tempfile
18+
import unittest
19+
from examples import measurements
20+
from openhtf.util import example_test
21+
22+
23+
class TestMeasurements(example_test.ExampleTestBase):
24+
25+
def test_main_execution(self):
26+
with tempfile.TemporaryDirectory() as temp_dir:
27+
measurements.create_and_run_test(temp_dir)
28+
expected_json_path = os.path.join(temp_dir, 'measurements.json')
29+
30+
# Assert that the output file was created
31+
self.assertTrue(os.path.exists(expected_json_path))
32+
with open(expected_json_path) as f:
33+
output_data = json.load(f)
34+
35+
self.assertEqual(output_data["dut_id"], "MyDutId")
36+
self.assertEqual(output_data["outcome"], "FAIL")
37+
self.assertIn("phases", output_data)
38+
self.assertEqual(len(output_data["phases"]), 7)
39+
40+
with self.subTest("hello_phase"):
41+
hello_phase_data = self.get_phase_by_name(output_data, "hello_phase")
42+
self.assertEqual(
43+
hello_phase_data["measurements"]["hello_world_measurement"][
44+
"measured_value"
45+
],
46+
"Hello!",
47+
)
48+
49+
with self.subTest("measure_seconds"):
50+
measure_seconds_data = self.get_phase_by_name(
51+
output_data, "measure_seconds"
52+
)
53+
self.assertEqual(
54+
measure_seconds_data["measurements"]["validated_measurement"][
55+
"outcome"
56+
],
57+
"PASS",
58+
)
59+
self.assertEqual(
60+
measure_seconds_data["measurements"]["validated_measurement"][
61+
"measured_value"
62+
],
63+
5,
64+
)
65+
66+
with self.subTest("inline_phase"):
67+
inline_phase_data = self.get_phase_by_name(output_data, "inline_phase")
68+
self.assertEqual(
69+
inline_phase_data["measurements"]["inline_kwargs"]["outcome"], "FAIL"
70+
)
71+
self.assertEqual(
72+
inline_phase_data["measurements"]["inline_kwargs"]["measured_value"],
73+
15,
74+
)
75+
76+
with self.subTest("multidim_measurements"):
77+
multdim_measurements_data = self.get_phase_by_name(
78+
output_data, "multdim_measurements"
79+
)
80+
self.assertAlmostEqual(
81+
multdim_measurements_data["measurements"]["average_voltage"][
82+
"measured_value"
83+
],
84+
55.0,
85+
)
86+
self.assertTrue(
87+
9
88+
<= multdim_measurements_data["measurements"]["resistance"][
89+
"measured_value"
90+
]
91+
<= 11
92+
)
93+
94+
95+
if __name__ == "__main__":
96+
unittest.main()

0 commit comments

Comments
 (0)