Skip to content

Commit 38ad07d

Browse files
Allow to communicate with DUT via standard input (project-chip#36687)
* Allow to communicate with DUT via standard input * Use fabric-sync-app.py stdin instead dedicated pipe * Drop pipe stdin forwarder in fabric-sync-app.py * Restyled by autopep8 * Wait for thread to stop * Fix referencing not-created variable --------- Co-authored-by: Restyled.io <commits@restyled.io>
1 parent 9e57208 commit 38ad07d

15 files changed

+82
-61
lines changed

docs/testing/python.md

+5
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,11 @@ for that run, e.g.:
722722

723723
- Example: `"Manual pairing code: \\[\\d+\\]"`
724724

725+
- `app-stdin-pipe`: Specifies the path to the named pipe that the test runner
726+
might use to send input to the application.
727+
728+
- Example: `/tmp/app-fifo`
729+
725730
- `script-args`: Specifies the arguments to be passed to the test script.
726731

727732
- Example:

examples/fabric-admin/scripts/fabric-sync-app.py

+1-36
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
import asyncio
1818
import contextlib
19-
import os
2019
import shutil
2120
import signal
2221
import sys
@@ -41,26 +40,6 @@ async def forward_f(prefix: bytes, f_in: asyncio.StreamReader,
4140
f_out.flush()
4241

4342

44-
async def forward_pipe(pipe_path: str, f_out: asyncio.StreamWriter):
45-
"""Forward named pipe to f_out.
46-
47-
Unfortunately, Python does not support async file I/O on named pipes. This
48-
function performs busy waiting with a short asyncio-friendly sleep to read
49-
from the pipe.
50-
"""
51-
fd = os.open(pipe_path, os.O_RDONLY | os.O_NONBLOCK)
52-
while True:
53-
try:
54-
data = os.read(fd, 1024)
55-
if data:
56-
f_out.write(data)
57-
await f_out.drain()
58-
if not data:
59-
await asyncio.sleep(0.1)
60-
except BlockingIOError:
61-
await asyncio.sleep(0.1)
62-
63-
6443
async def forward_stdin(f_out: asyncio.StreamWriter):
6544
"""Forward stdin to f_out."""
6645
loop = asyncio.get_event_loop()
@@ -175,9 +154,6 @@ async def main(args):
175154
storage = TemporaryDirectory(prefix="fabric-sync-app")
176155
storage_dir = Path(storage.name)
177156

178-
if args.stdin_pipe and not args.stdin_pipe.exists():
179-
os.mkfifo(args.stdin_pipe)
180-
181157
admin, bridge = await asyncio.gather(
182158
run_admin(
183159
args.app_admin,
@@ -206,8 +182,6 @@ def terminate():
206182
admin.terminate()
207183
with contextlib.suppress(ProcessLookupError):
208184
bridge.terminate()
209-
if args.stdin_pipe:
210-
args.stdin_pipe.unlink(missing_ok=True)
211185
loop.remove_signal_handler(signal.SIGINT)
212186
loop.remove_signal_handler(signal.SIGTERM)
213187

@@ -249,17 +223,12 @@ def terminate():
249223
await admin.send(f"pairing open-commissioning-window {bridge_node_id} {cw_endpoint_id}"
250224
f" {cw_option} {cw_timeout} {cw_iteration} {cw_discriminator}")
251225

252-
def get_input_forwarder():
253-
if args.stdin_pipe:
254-
return forward_pipe(args.stdin_pipe, admin.p.stdin)
255-
return forward_stdin(admin.p.stdin)
256-
257226
try:
258227
# Wait for any of the tasks to complete.
259228
_, pending = await asyncio.wait([
260229
asyncio.create_task(admin.wait()),
261230
asyncio.create_task(bridge.wait()),
262-
asyncio.create_task(get_input_forwarder()),
231+
asyncio.create_task(forward_stdin(admin.p.stdin)),
263232
], return_when=asyncio.FIRST_COMPLETED)
264233
# Cancel the remaining tasks.
265234
for task in pending:
@@ -285,8 +254,6 @@ def get_input_forwarder():
285254
help="fabric-admin RPC server port")
286255
parser.add_argument("--app-bridge-rpc-port", metavar="PORT", type=int,
287256
help="fabric-bridge RPC server port")
288-
parser.add_argument("--stdin-pipe", metavar="PATH", type=Path,
289-
help="read input from a named pipe instead of stdin")
290257
parser.add_argument("--storage-dir", metavar="PATH", type=Path,
291258
help=("directory to place storage files in; by default "
292259
"volatile storage is used"))
@@ -309,7 +276,5 @@ def get_input_forwarder():
309276
parser.error("fabric-admin executable not found in PATH. Use '--app-admin' argument to provide it.")
310277
if args.app_bridge is None or not args.app_bridge.exists():
311278
parser.error("fabric-bridge-app executable not found in PATH. Use '--app-bridge' argument to provide it.")
312-
if args.stdin_pipe and args.stdin_pipe.exists() and not args.stdin_pipe.is_fifo():
313-
parser.error("given stdin pipe exists and is not a named pipe")
314279
with contextlib.suppress(KeyboardInterrupt):
315280
asyncio.run(main(args))

scripts/tests/run_python_test.py

+42-4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616

17+
import contextlib
1718
import datetime
1819
import glob
1920
import io
@@ -22,9 +23,12 @@
2223
import os.path
2324
import pathlib
2425
import re
26+
import select
2527
import shlex
2628
import sys
29+
import threading
2730
import time
31+
import typing
2832

2933
import click
3034
import coloredlogs
@@ -68,6 +72,23 @@ def process_test_script_output(line, is_stderr):
6872
return process_chip_output(line, is_stderr, TAG_PROCESS_TEST)
6973

7074

75+
def forward_fifo(path: str, f_out: typing.BinaryIO, stop_event: threading.Event):
76+
"""Forward the content of a named pipe to a file-like object."""
77+
if not os.path.exists(path):
78+
with contextlib.suppress(OSError):
79+
os.mkfifo(path)
80+
with open(os.open(path, os.O_RDONLY | os.O_NONBLOCK), 'rb') as f_in:
81+
while not stop_event.is_set():
82+
if select.select([f_in], [], [], 0.5)[0]:
83+
line = f_in.readline()
84+
if not line:
85+
break
86+
f_out.write(line)
87+
f_out.flush()
88+
with contextlib.suppress(OSError):
89+
os.unlink(path)
90+
91+
7192
@click.command()
7293
@click.option("--app", type=click.Path(exists=True), default=None,
7394
help='Path to local application to use, omit to use external apps.')
@@ -79,6 +100,8 @@ def process_test_script_output(line, is_stderr):
79100
help='The extra arguments passed to the device. Can use placeholders like {SCRIPT_BASE_NAME}')
80101
@click.option("--app-ready-pattern", type=str, default=None,
81102
help='Delay test script start until given regular expression pattern is found in the application output.')
103+
@click.option("--app-stdin-pipe", type=str, default=None,
104+
help='Path for a standard input redirection named pipe to be used by the test script.')
82105
@click.option("--script", type=click.Path(exists=True), default=os.path.join(DEFAULT_CHIP_ROOT,
83106
'src',
84107
'controller',
@@ -94,7 +117,8 @@ def process_test_script_output(line, is_stderr):
94117
help="Do not print output from passing tests. Use this flag in CI to keep GitHub log size manageable.")
95118
@click.option("--load-from-env", default=None, help="YAML file that contains values for environment variables.")
96119
def main(app: str, factory_reset: bool, factory_reset_app_only: bool, app_args: str,
97-
app_ready_pattern: str, script: str, script_args: str, script_gdb: bool, quiet: bool, load_from_env):
120+
app_ready_pattern: str, app_stdin_pipe: str, script: str, script_args: str,
121+
script_gdb: bool, quiet: bool, load_from_env):
98122
if load_from_env:
99123
reader = MetadataReader(load_from_env)
100124
runs = reader.parse_script(script)
@@ -106,6 +130,7 @@ def main(app: str, factory_reset: bool, factory_reset_app_only: bool, app_args:
106130
app=app,
107131
app_args=app_args,
108132
app_ready_pattern=app_ready_pattern,
133+
app_stdin_pipe=app_stdin_pipe,
109134
script_args=script_args,
110135
script_gdb=script_gdb,
111136
)
@@ -128,11 +153,13 @@ def main(app: str, factory_reset: bool, factory_reset_app_only: bool, app_args:
128153
for run in runs:
129154
logging.info("Executing %s %s", run.py_script_path.split('/')[-1], run.run)
130155
main_impl(run.app, run.factory_reset, run.factory_reset_app_only, run.app_args or "",
131-
run.app_ready_pattern, run.py_script_path, run.script_args or "", run.script_gdb, run.quiet)
156+
run.app_ready_pattern, run.app_stdin_pipe, run.py_script_path,
157+
run.script_args or "", run.script_gdb, run.quiet)
132158

133159

134160
def main_impl(app: str, factory_reset: bool, factory_reset_app_only: bool, app_args: str,
135-
app_ready_pattern: str, script: str, script_args: str, script_gdb: bool, quiet: bool):
161+
app_ready_pattern: str, app_stdin_pipe: str, script: str, script_args: str,
162+
script_gdb: bool, quiet: bool):
136163

137164
app_args = app_args.replace('{SCRIPT_BASE_NAME}', os.path.splitext(os.path.basename(script))[0])
138165
script_args = script_args.replace('{SCRIPT_BASE_NAME}', os.path.splitext(os.path.basename(script))[0])
@@ -154,6 +181,8 @@ def main_impl(app: str, factory_reset: bool, factory_reset_app_only: bool, app_a
154181
pathlib.Path(match.group("path")).unlink(missing_ok=True)
155182

156183
app_process = None
184+
app_stdin_forwarding_thread = None
185+
app_stdin_forwarding_stop_event = threading.Event()
157186
app_exit_code = 0
158187
app_pid = 0
159188

@@ -172,7 +201,13 @@ def main_impl(app: str, factory_reset: bool, factory_reset_app_only: bool, app_a
172201
f_stdout=stream_output,
173202
f_stderr=stream_output)
174203
app_process.start(expected_output=app_ready_pattern, timeout=30)
175-
app_process.p.stdin.close()
204+
if app_stdin_pipe:
205+
logging.info("Forwarding stdin from '%s' to app", app_stdin_pipe)
206+
app_stdin_forwarding_thread = threading.Thread(
207+
target=forward_fifo, args=(app_stdin_pipe, app_process.p.stdin, app_stdin_forwarding_stop_event))
208+
app_stdin_forwarding_thread.start()
209+
else:
210+
app_process.p.stdin.close()
176211
app_pid = app_process.p.pid
177212

178213
script_command = [script, "--paa-trust-store-path", os.path.join(DEFAULT_CHIP_ROOT, MATTER_DEVELOPMENT_PAA_ROOT_CERTS),
@@ -204,6 +239,9 @@ def main_impl(app: str, factory_reset: bool, factory_reset_app_only: bool, app_a
204239

205240
if app_process:
206241
logging.info("Stopping app with SIGTERM")
242+
if app_stdin_forwarding_thread:
243+
app_stdin_forwarding_stop_event.set()
244+
app_stdin_forwarding_thread.join()
207245
app_process.terminate()
208246
app_exit_code = app_process.returncode
209247

src/python_testing/TC_BRBINFO_4_1.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
# test-runner-runs:
2323
# run1:
2424
# app: examples/fabric-admin/scripts/fabric-sync-app.py
25-
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa-stdin --discriminator=1234
25+
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --discriminator=1234
2626
# app-ready-pattern: "Successfully opened pairing window on the device"
27+
# app-stdin-pipe: dut-fsa-stdin
2728
# script-args: >
2829
# --PICS src/app/tests/suites/certification/ci-pics-values
2930
# --storage-path admin_storage.json

src/python_testing/TC_CCTRL_2_1.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
# test-runner-runs:
2323
# run1:
2424
# app: examples/fabric-admin/scripts/fabric-sync-app.py
25-
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa-stdin --discriminator=1234
25+
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --discriminator=1234
2626
# app-ready-pattern: "Successfully opened pairing window on the device"
27+
# app-stdin-pipe: dut-fsa-stdin
2728
# script-args: >
2829
# --PICS src/app/tests/suites/certification/ci-pics-values
2930
# --storage-path admin_storage.json

src/python_testing/TC_CCTRL_2_2.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
# test-runner-runs:
2323
# run1:
2424
# app: examples/fabric-admin/scripts/fabric-sync-app.py
25-
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa-stdin --discriminator=1234
25+
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --discriminator=1234
2626
# app-ready-pattern: "Successfully opened pairing window on the device"
27+
# app-stdin-pipe: dut-fsa-stdin
2728
# script-args: >
2829
# --PICS src/app/tests/suites/certification/ci-pics-values
2930
# --storage-path admin_storage.json

src/python_testing/TC_CCTRL_2_3.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
# test-runner-runs:
2323
# run1:
2424
# app: examples/fabric-admin/scripts/fabric-sync-app.py
25-
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa-stdin --discriminator=1234
25+
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --discriminator=1234
2626
# app-ready-pattern: "Successfully opened pairing window on the device"
27+
# app-stdin-pipe: dut-fsa-stdin
2728
# script-args: >
2829
# --PICS src/app/tests/suites/certification/ci-pics-values
2930
# --storage-path admin_storage.json

src/python_testing/TC_ECOINFO_2_1.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
# test-runner-runs:
2323
# run1:
2424
# app: examples/fabric-admin/scripts/fabric-sync-app.py
25-
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa-stdin --discriminator=1234
25+
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --discriminator=1234
2626
# app-ready-pattern: "Successfully opened pairing window on the device"
27+
# app-stdin-pipe: dut-fsa-stdin
2728
# script-args: >
2829
# --PICS src/app/tests/suites/certification/ci-pics-values
2930
# --storage-path admin_storage.json

src/python_testing/TC_ECOINFO_2_2.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
# test-runner-runs:
2323
# run1:
2424
# app: examples/fabric-admin/scripts/fabric-sync-app.py
25-
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa-stdin --discriminator=1234
25+
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --discriminator=1234
2626
# app-ready-pattern: "Successfully opened pairing window on the device"
27+
# app-stdin-pipe: dut-fsa-stdin
2728
# script-args: >
2829
# --PICS src/app/tests/suites/certification/ci-pics-values
2930
# --storage-path admin_storage.json

src/python_testing/TC_MCORE_FS_1_1.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@
2424
# test-runner-runs:
2525
# run1:
2626
# app: examples/fabric-admin/scripts/fabric-sync-app.py
27-
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa-stdin --discriminator=1234
27+
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --discriminator=1234
2828
# app-ready-pattern: "Successfully opened pairing window on the device"
29+
# app-stdin-pipe: dut-fsa-stdin
2930
# script-args: >
3031
# --PICS src/app/tests/suites/certification/ci-pics-values
3132
# --storage-path admin_storage.json

src/python_testing/TC_MCORE_FS_1_2.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
# test-runner-runs:
2323
# run1:
2424
# app: examples/fabric-admin/scripts/fabric-sync-app.py
25-
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa-stdin --discriminator=1234
25+
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --discriminator=1234
2626
# app-ready-pattern: "Successfully opened pairing window on the device"
27+
# app-stdin-pipe: dut-fsa-stdin
2728
# script-args: >
2829
# --PICS src/app/tests/suites/certification/ci-pics-values
2930
# --storage-path admin_storage.json

src/python_testing/TC_MCORE_FS_1_3.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@
2626
# test-runner-runs:
2727
# run1:
2828
# app: examples/fabric-admin/scripts/fabric-sync-app.py
29-
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa-stdin --discriminator=1234
29+
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --discriminator=1234
3030
# app-ready-pattern: "Successfully opened pairing window on the device"
31+
# app-stdin-pipe: dut-fsa-stdin
3132
# script-args: >
3233
# --PICS src/app/tests/suites/certification/ci-pics-values
3334
# --storage-path admin_storage.json
@@ -71,11 +72,11 @@ def setup_class(self):
7172
self.storage = None
7273

7374
# Get the path to the TH_SERVER_NO_UID app from the user params.
74-
th_server_app = self.user_params.get("th_server_no_uid_app_path", None)
75-
if not th_server_app:
75+
th_server_no_uid_app = self.user_params.get("th_server_no_uid_app_path", None)
76+
if not th_server_no_uid_app:
7677
asserts.fail("This test requires a TH_SERVER_NO_UID app. Specify app path with --string-arg th_server_no_uid_app_path:<path_to_app>")
77-
if not os.path.exists(th_server_app):
78-
asserts.fail(f"The path {th_server_app} does not exist")
78+
if not os.path.exists(th_server_no_uid_app):
79+
asserts.fail(f"The path {th_server_no_uid_app} does not exist")
7980

8081
# Create a temporary storage directory for keeping KVS files.
8182
self.storage = tempfile.TemporaryDirectory(prefix=self.__class__.__name__)
@@ -94,7 +95,7 @@ def setup_class(self):
9495

9596
# Start the TH_SERVER_NO_UID app.
9697
self.th_server = AppServerSubprocess(
97-
th_server_app,
98+
th_server_no_uid_app,
9899
storage_dir=self.storage.name,
99100
port=self.th_server_port,
100101
discriminator=self.th_server_discriminator,

src/python_testing/TC_MCORE_FS_1_4.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@
2626
# test-runner-runs:
2727
# run1:
2828
# app: examples/fabric-admin/scripts/fabric-sync-app.py
29-
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa-stdin --discriminator=1234
29+
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --discriminator=1234
3030
# app-ready-pattern: "Successfully opened pairing window on the device"
31+
# app-stdin-pipe: dut-fsa-stdin
3132
# script-args: >
3233
# --PICS src/app/tests/suites/certification/ci-pics-values
3334
# --storage-path admin_storage.json
@@ -129,11 +130,11 @@ def setup_class(self):
129130
asserts.fail(f"The path {th_fsa_bridge_path} does not exist")
130131

131132
# Get the path to the TH_SERVER_NO_UID app from the user params.
132-
th_server_app = self.user_params.get("th_server_no_uid_app_path", None)
133-
if not th_server_app:
133+
th_server_no_uid_app = self.user_params.get("th_server_no_uid_app_path", None)
134+
if not th_server_no_uid_app:
134135
asserts.fail("This test requires a TH_SERVER_NO_UID app. Specify app path with --string-arg th_server_no_uid_app_path:<path_to_app>")
135-
if not os.path.exists(th_server_app):
136-
asserts.fail(f"The path {th_server_app} does not exist")
136+
if not os.path.exists(th_server_no_uid_app):
137+
asserts.fail(f"The path {th_server_no_uid_app} does not exist")
137138

138139
# Create a temporary storage directory for keeping KVS files.
139140
self.storage = tempfile.TemporaryDirectory(prefix=self.__class__.__name__)
@@ -171,7 +172,7 @@ def setup_class(self):
171172

172173
# Start the TH_SERVER_NO_UID app.
173174
self.th_server = AppServerSubprocess(
174-
th_server_app,
175+
th_server_no_uid_app,
175176
storage_dir=self.storage.name,
176177
port=self.th_server_port,
177178
discriminator=self.th_server_discriminator,

src/python_testing/TC_MCORE_FS_1_5.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
# test-runner-runs:
2323
# run1:
2424
# app: examples/fabric-admin/scripts/fabric-sync-app.py
25-
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa-stdin --discriminator=1234
25+
# app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --discriminator=1234
2626
# app-ready-pattern: "Successfully opened pairing window on the device"
27+
# app-stdin-pipe: dut-fsa-stdin
2728
# script-args: >
2829
# --PICS src/app/tests/suites/certification/ci-pics-values
2930
# --storage-path admin_storage.json

0 commit comments

Comments
 (0)