Skip to content

Commit 7cdea9b

Browse files
Create test script to verify that the Linux tv-casting-app is able to discover the Linux tv-app. (#32919)
1 parent f9a3601 commit 7cdea9b

File tree

2 files changed

+232
-1
lines changed

2 files changed

+232
-1
lines changed

.github/workflows/examples-linux-tv-casting-app.yaml

+7-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ name: Test TV Casting Example
1717
on:
1818
push:
1919
branches-ignore:
20-
- 'dependabot/**'
20+
- "dependabot/**"
2121
pull_request:
2222
merge_group:
2323

@@ -63,6 +63,12 @@ jobs:
6363
./scripts/run_in_build_env.sh \
6464
"scripts/examples/gn_build_example.sh examples/tv-casting-app/linux/ out/tv-casting-app"
6565
66+
- name: Test casting from Linux tv-casting-app to Linux tv-app
67+
run: |
68+
./scripts/run_in_build_env.sh \
69+
"python3 ./scripts/tests/run_tv_casting_test.py"
70+
timeout-minutes: 1
71+
6672
- name: Uploading Size Reports
6773
uses: ./.github/actions/upload-size-reports
6874
if: ${{ !env.ACT }}

scripts/tests/run_tv_casting_test.py

+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
#!/usr/bin/env -S python3 -B
2+
3+
# Copyright (c) 2024 Project CHIP Authors
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
import logging
18+
import os
19+
import subprocess
20+
import sys
21+
import tempfile
22+
import time
23+
from typing import List, Optional, TextIO, Tuple
24+
25+
import click
26+
27+
# Configure logging format.
28+
logging.basicConfig(level=logging.INFO, format='%(levelname)s - %(message)s')
29+
30+
# The maximum amount of time to wait for the Linux tv-app to start before timeout.
31+
TV_APP_MAX_START_WAIT_SEC = 2
32+
33+
# File names of logs for the Linux tv-casting-app and the Linux tv-app.
34+
LINUX_TV_APP_LOGS = 'Linux-tv-app-logs.txt'
35+
LINUX_TV_CASTING_APP_LOGS = 'Linux-tv-casting-app-logs.txt'
36+
37+
# Values that identify the Linux tv-app and are noted in the 'Device Configuration' in the Linux tv-app output
38+
# as well as under the 'Discovered Commissioner' details in the Linux tv-casting-app output.
39+
VENDOR_ID = 0xFFF1 # Spec 7.20.2.1 MEI code: test vendor IDs are 0xFFF1 to 0xFFF4
40+
PRODUCT_ID = 0x8001 # Test product id
41+
DEVICE_TYPE_CASTING_VIDEO_PLAYER = 0x23 # Device type library 10.3: Casting Video Player
42+
43+
44+
class ProcessManager:
45+
"""A context manager for managing subprocesses.
46+
47+
This class provides a context manager for safely starting and stopping a subprocess.
48+
"""
49+
50+
def __init__(self, command: List[str], stdout, stderr):
51+
self.command = command
52+
self.stdout = stdout
53+
self.stderr = stderr
54+
55+
def __enter__(self):
56+
self.process = subprocess.Popen(self.command, stdout=self.stdout, stderr=self.stderr, text=True)
57+
return self.process
58+
59+
def __exit__(self, exception_type, exception_value, traceback):
60+
self.process.terminate()
61+
self.process.wait()
62+
63+
64+
def dump_temporary_logs_to_console(log_file_path: str):
65+
"""Dump log file to the console; log file will be removed once the function exits."""
66+
"""Write the entire content of `log_file_path` to the console."""
67+
print('\nDumping logs from: ', log_file_path)
68+
69+
with open(log_file_path, 'r') as file:
70+
for line in file:
71+
print(line.rstrip())
72+
73+
74+
def handle_discovery_failure(log_file_paths: List[str]):
75+
"""Log 'Discovery failed!' as error, dump log files to console, exit on error."""
76+
logging.error('Discovery failed!')
77+
78+
for log_file_path in log_file_paths:
79+
try:
80+
dump_temporary_logs_to_console(log_file_path)
81+
except Exception as e:
82+
logging.exception(f"Failed to dump {log_file_path}: {e}")
83+
84+
sys.exit(1)
85+
86+
87+
def extract_value_from_string(line: str) -> int:
88+
"""Extract and return integer value from given output string.
89+
90+
The string is expected to be in the following format as it is received
91+
from the Linux tv-casting-app output:
92+
\x1b[0;34m[1713741926895] [7276:9521344] [DIS] Vendor ID: 65521\x1b[0m
93+
The integer value to be extracted here is 65521.
94+
"""
95+
value = line.split(':')[-1].strip().replace('\x1b[0m', '')
96+
value = int(value)
97+
98+
return value
99+
100+
101+
def validate_value(expected_value: int, log_paths: List[str], line: str, value_name: str) -> Optional[str]:
102+
"""Validate a value in a string against an expected value."""
103+
value = extract_value_from_string(line)
104+
105+
if value != expected_value:
106+
logging.error(f'{value_name} does not match the expected value!')
107+
logging.error(f'Expected {value_name}: {expected_value}')
108+
logging.error(line.rstrip('\n'))
109+
handle_discovery_failure(log_paths)
110+
return None
111+
112+
# Return the line containing the valid value.
113+
return line.rstrip('\n')
114+
115+
116+
def start_up_tv_app_success(tv_app_process: subprocess.Popen, linux_tv_app_log_file: TextIO) -> bool:
117+
"""Check if the Linux tv-app is able to successfully start or until timeout occurs."""
118+
start_wait_time = time.time()
119+
120+
while True:
121+
# Check if the time elapsed since the start wait time exceeds the maximum allowed startup time for the TV app.
122+
if time.time() - start_wait_time > TV_APP_MAX_START_WAIT_SEC:
123+
logging.error("The Linux tv-app process did not start successfully within the timeout.")
124+
return False
125+
126+
tv_app_output_line = tv_app_process.stdout.readline()
127+
128+
linux_tv_app_log_file.write(tv_app_output_line)
129+
linux_tv_app_log_file.flush()
130+
131+
# Check if the Linux tv-app started successfully.
132+
if "Started commissioner" in tv_app_output_line:
133+
logging.info('Linux tv-app is up and running!')
134+
return True
135+
136+
137+
def parse_output_for_valid_commissioner(tv_casting_app_info: Tuple[subprocess.Popen, TextIO], log_paths: List[str]):
138+
"""Parse the output of the Linux tv-casting-app to find a valid commissioner."""
139+
tv_casting_app_process, linux_tv_casting_app_log_file = tv_casting_app_info
140+
141+
valid_discovered_commissioner = None
142+
valid_vendor_id = None
143+
valid_product_id = None
144+
valid_device_type = None
145+
146+
# Read the output as we receive it from the tv-casting-app subprocess.
147+
for line in tv_casting_app_process.stdout:
148+
linux_tv_casting_app_log_file.write(line)
149+
linux_tv_casting_app_log_file.flush()
150+
151+
# Fail fast if "No commissioner discovered" string found.
152+
if "No commissioner discovered" in line:
153+
logging.error(line.rstrip('\n'))
154+
handle_discovery_failure(log_paths)
155+
156+
elif "Discovered Commissioner" in line:
157+
valid_discovered_commissioner = line.rstrip('\n')
158+
159+
elif valid_discovered_commissioner:
160+
# Continue parsing the output for the information of interest under 'Discovered Commissioner'
161+
if 'Vendor ID:' in line:
162+
valid_vendor_id = validate_value(VENDOR_ID, log_paths, line, 'Vendor ID')
163+
164+
elif 'Product ID:' in line:
165+
valid_product_id = validate_value(PRODUCT_ID, log_paths, line, 'Product ID')
166+
167+
elif 'Device Type:' in line:
168+
valid_device_type = validate_value(DEVICE_TYPE_CASTING_VIDEO_PLAYER, log_paths, line, 'Device Type')
169+
170+
# A valid commissioner has VENDOR_ID, PRODUCT_ID, and DEVICE TYPE in its list of entries.
171+
if valid_vendor_id and valid_product_id and valid_device_type:
172+
logging.info('Found a valid commissioner in the Linux tv-casting-app logs:')
173+
logging.info(valid_discovered_commissioner)
174+
logging.info(valid_vendor_id)
175+
logging.info(valid_product_id)
176+
logging.info(valid_device_type)
177+
logging.info('Discovery success!')
178+
break
179+
180+
181+
@click.command()
182+
@click.option('--tv-app-rel-path', type=str, default='out/tv-app/chip-tv-app', help='Path to the Linux tv-app executable.')
183+
@click.option('--tv-casting-app-rel-path', type=str, default='out/tv-casting-app/chip-tv-casting-app', help='Path to the Linux tv-casting-app executable.')
184+
def test_discovery_fn(tv_app_rel_path, tv_casting_app_rel_path):
185+
"""Test if the Linux tv-casting-app is able to discover the Linux tv-app.
186+
187+
Default paths for the executables are provided but can be overridden via command line arguments.
188+
For example: python3 run_tv_casting_test.py --tv-app-rel-path=path/to/tv-app
189+
--tv-casting-app-rel-path=path/to/tv-casting-app
190+
"""
191+
# Store the log files to a temporary directory.
192+
with tempfile.TemporaryDirectory() as temp_dir:
193+
linux_tv_app_log_path = os.path.join(temp_dir, LINUX_TV_APP_LOGS)
194+
linux_tv_casting_app_log_path = os.path.join(temp_dir, LINUX_TV_CASTING_APP_LOGS)
195+
196+
with open(linux_tv_app_log_path, 'w') as linux_tv_app_log_file, open(linux_tv_casting_app_log_path, 'w') as linux_tv_casting_app_log_file:
197+
198+
# Configure command options to disable stdout buffering during tests.
199+
disable_stdout_buffering_cmd = []
200+
# On Unix-like systems, use stdbuf to disable stdout buffering.
201+
if sys.platform == 'darwin' or sys.platform == 'linux':
202+
disable_stdout_buffering_cmd = ['stdbuf', '-o0', '-i0']
203+
204+
tv_app_abs_path = os.path.abspath(tv_app_rel_path)
205+
# Run the Linux tv-app subprocess.
206+
with ProcessManager(disable_stdout_buffering_cmd + [tv_app_abs_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) as tv_app_process:
207+
208+
if not start_up_tv_app_success(tv_app_process, linux_tv_app_log_file):
209+
handle_discovery_failure([linux_tv_app_log_path])
210+
211+
tv_casting_app_abs_path = os.path.abspath(tv_casting_app_rel_path)
212+
# Run the Linux tv-casting-app subprocess.
213+
with ProcessManager(disable_stdout_buffering_cmd + [tv_casting_app_abs_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) as tv_casting_app_process:
214+
log_paths = [linux_tv_app_log_path, linux_tv_casting_app_log_path]
215+
tv_casting_app_info = (tv_casting_app_process, linux_tv_casting_app_log_file)
216+
parse_output_for_valid_commissioner(tv_casting_app_info, log_paths)
217+
218+
219+
if __name__ == '__main__':
220+
221+
# Start with a clean slate by removing any previously cached entries.
222+
os.system('rm -f /tmp/chip_*')
223+
224+
# Test discovery between the Linux tv-casting-app and the tv-app.
225+
test_discovery_fn()

0 commit comments

Comments
 (0)