Skip to content

Commit 0ca28b1

Browse files
Testing changes locally to see if CI check succeeds.
1 parent 5e925ca commit 0ca28b1

File tree

2 files changed

+314
-1
lines changed

2 files changed

+314
-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

+307
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
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 pdb
20+
import subprocess
21+
import sys
22+
import tempfile
23+
import time
24+
from typing import List, Optional
25+
26+
import click
27+
28+
# Configure logging format.
29+
logging.basicConfig(level=logging.INFO, format='%(levelname)s - %(message)s')
30+
31+
# The maximum amount of time to wait for the Linux tv-app to start before timeout.
32+
TV_APP_MAX_START_WAIT_SEC = 2
33+
34+
# File names of logs for the Linux tv-casting-app and the Linux tv-app.
35+
LINUX_TV_APP_LOGS = 'Linux-tv-app-logs.txt'
36+
LINUX_TV_CASTING_APP_LOGS = 'Linux-tv-casting-app-logs.txt'
37+
38+
# Values that identify the Linux tv-app and are noted in the 'Device Configuration' in the Linux tv-app output
39+
# as well as under the 'Discovered Commissioner' details in the Linux tv-casting-app output.
40+
VENDOR_ID = 65521 # Test vendor id
41+
PRODUCT_ID = 32769 # Test product id
42+
DEVICE_TYPE = 35 # Casting video player
43+
44+
45+
class LogFileManager:
46+
"""
47+
A context manager for managing log files.
48+
49+
This class provides a context manager for safely opening and closing log files.
50+
It ensures that log files are properly managed, allowing reading, writing, or
51+
both, depending on the specified mode.
52+
"""
53+
54+
# Initialize LogFileManager.
55+
# log_file_path (str): The path to the log file.
56+
# mode (str): The file mode for opening the log file (default is 'w+' for read/write mode).
57+
def __init__(self, log_file_path: str, mode: str = 'w+'):
58+
self.log_file_path = log_file_path
59+
self.mode = mode
60+
61+
# Enter the context manager to open and return the log file.
62+
def __enter__(self):
63+
try:
64+
self.file = open(self.log_file_path, self.mode)
65+
except FileNotFoundError:
66+
# Handle file not found error
67+
raise FileNotFoundError(f"Log file '{self.log_file_path}' not found.")
68+
except IOError:
69+
# Handle IO error
70+
raise IOError(f"Error opening log file '{self.log_file_path}'.")
71+
return self.file
72+
73+
# Exit the context manager, closing and removing the log file created.
74+
# exception_type: The type of exception that occurred, if any.
75+
# exception_value: The value of the exception, if any.
76+
# traceback: The traceback of the exception.
77+
def __exit__(self, exception_type, exception_value, traceback):
78+
self.file.close()
79+
80+
if os.path.exists(self.log_file_path):
81+
os.remove(self.log_file_path)
82+
83+
84+
class ProcessManager:
85+
"""
86+
A context manager for managing subprocesses.
87+
88+
This class provides a context manager for safely starting and stopping a subprocess.
89+
"""
90+
91+
# Initialize ProcessManager.
92+
# command (list): The command to execute as a subprocess.
93+
# stdout (file): File-like object to which the subprocess's standard output will be redirected.
94+
# stderr (file): File-like object to which the subprocess's standard error will be redirected.
95+
def __init__(self, command: List[str], stdout, stderr):
96+
self.command = command
97+
self.stdout = stdout
98+
self.stderr = stderr
99+
100+
# Enter the context manager to start the subprocess and return it.
101+
def __enter__(self):
102+
self.process = subprocess.Popen(self.command, stdout=self.stdout, stderr=self.stderr, text=True)
103+
return self.process
104+
105+
# Exit the context manager, terminating the subprocess.
106+
# exception_type: The type of exception that occurred, if any.
107+
# exception_value: The value of the exception, if any.
108+
# traceback: The traceback of the exception.
109+
def __exit__(self, exception_type, exception_value, traceback):
110+
self.process.terminate()
111+
self.process.wait()
112+
113+
114+
# Dump the contents of a log file to the console.
115+
# log_file_path: The path to the log file.
116+
def dump_logs_to_console(log_file_path: str):
117+
print('\nDumping logs from: ', log_file_path)
118+
119+
with LogFileManager(log_file_path, 'r') as file:
120+
for line in file:
121+
print(line.rstrip())
122+
123+
124+
# Log 'Discovery failed!' as an error, dump the contents of the log files
125+
# to the console, exit on error.
126+
# log_file_paths: A list of paths to the log files, i.e. the path to the
127+
# Linux tv-casting-app logs and the tv-app logs.
128+
def handle_discovery_failure(log_file_paths: List[str]):
129+
logging.error('Discovery failed!')
130+
131+
for log_file_path in log_file_paths:
132+
dump_logs_to_console(log_file_path)
133+
134+
sys.exit(1)
135+
136+
137+
# Extract and return an integer value from a given output string.
138+
# line: The string containing the integer value.
139+
#
140+
# The string is expected to be in the following format as it is received
141+
# from the Linux tv-casting-app output:
142+
# \x1b[0;34m[1713741926895] [7276:9521344] [DIS] Vendor ID: 65521\x1b[0m
143+
# The integer value to be extracted here is 65521.
144+
def extract_value_from_string(line: str) -> int:
145+
value = line.split(':')[-1].strip().replace('\x1b[0m', '')
146+
value = int(value)
147+
148+
return value
149+
150+
151+
# Validate if the discovered value matches the expected value.
152+
# expected_value: The expected integer value, i.e. any of the VENDOR_ID,
153+
# PRODUCT_ID, or DEVICE_TYPE constants.
154+
# line: The string containing the value of interest that will be compared
155+
# to the expected value.
156+
# value_name: The name of the discovered value, i.e. 'Vendor ID', 'Product ID',
157+
# or 'Device Type'.
158+
# Return False if the discovered value does not match, True otherwise.
159+
def validate_value(expected_value: int, line: str, value_name: str) -> bool:
160+
# Extract the integer value from the string.
161+
value = extract_value_from_string(line)
162+
163+
# If the discovered value does not match the expected value,
164+
# log the error and return False.
165+
if value != expected_value:
166+
logging.error(f'{value_name} does not match the expected value!')
167+
logging.error(f'Expected {value_name}: {expected_value}')
168+
logging.error(line.rstrip('\n'))
169+
return False
170+
171+
# Return True if the value matches the expected value.
172+
return True
173+
174+
175+
# Test if the Linux tv-casting-app is able to discover the Linux tv-app. Both will
176+
# run separately as subprocesses, with their outputs written to respective log files.
177+
# Default paths for the executables are provided but can be overridden via command line
178+
# arguments. For example: python3 run_tv_casting_test.py --tv-app-rel-path=path/to/tv-app
179+
# --tv-casting-app-rel-path=path/to/tv-casting-app
180+
@click.command()
181+
@click.option('--tv-app-rel-path', type=str, default='out/tv-app/chip-tv-app', help='Path to the Linux tv-app executable.')
182+
@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.')
183+
def test_discovery_fn(tv_app_rel_path, tv_casting_app_rel_path):
184+
185+
# Store the log files to a temporary directory.
186+
with tempfile.TemporaryDirectory() as temp_dir:
187+
linux_tv_app_log_path = os.path.join(temp_dir, LINUX_TV_APP_LOGS)
188+
linux_tv_casting_app_log_path = os.path.join(temp_dir, LINUX_TV_CASTING_APP_LOGS)
189+
190+
print(temp_dir) # SHAO
191+
192+
# Open and write to the log file for the Linux tv-app.
193+
with LogFileManager(linux_tv_app_log_path, 'w') as linux_tv_app_log_file:
194+
tv_app_abs_path = os.path.abspath(tv_app_rel_path)
195+
196+
if sys.platform == 'darwin':
197+
# Try to avoid any stdout buffering in our tests.
198+
cmd = ['stdbuf', '-o0', '-i0']
199+
200+
# Run the Linux tv-app subprocess.
201+
with ProcessManager(cmd + [tv_app_abs_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) as tv_app_process:
202+
start_wait_time = time.time()
203+
204+
# Loop until either the subprocess starts successfully or timeout occurs.
205+
while True:
206+
# Check if the time elapsed since the start wait time exceeds the maximum allowed startup time for the TV app.
207+
if time.time() - start_wait_time > TV_APP_MAX_START_WAIT_SEC:
208+
logging.error("The Linux tv-app process did not start successfully within the timeout.")
209+
handle_discovery_failure([linux_tv_app_log_path])
210+
211+
# Read one line of output at a time.
212+
tv_app_output_line = tv_app_process.stdout.readline()
213+
214+
# Write the output to the file.
215+
linux_tv_app_log_file.write(tv_app_output_line)
216+
linux_tv_app_log_file.flush()
217+
218+
# Check if the Linux tv-app started successfully.
219+
if "Started commissioner" in tv_app_output_line:
220+
logging.info('Linux tv-app is up and running!')
221+
222+
# If the string is found, then break out of the loop and go ahead with running the Linux tv-casting-app.
223+
break
224+
225+
# Open and write to the log file for the Linux tv-casting-app.
226+
with LogFileManager(linux_tv_casting_app_log_path, 'w') as linux_tv_casting_app_log_file:
227+
tv_casting_app_abs_path = os.path.abspath(tv_casting_app_rel_path)
228+
229+
# Run the Linux tv-casting-app subprocess.
230+
with ProcessManager(cmd + [tv_casting_app_abs_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) as tv_casting_app_process:
231+
# Initialize variables.
232+
continue_parsing = False
233+
valid_discovered_commissioner = ''
234+
235+
# Read the output as we receive it from the tv-casting-app subprocess.
236+
for line in tv_casting_app_process.stdout:
237+
# Write to the Linux tv-casting-app log file.
238+
linux_tv_casting_app_log_file.write(line)
239+
linux_tv_casting_app_log_file.flush()
240+
241+
# Fail fast if "No commissioner discovered" string found.
242+
if "No commissioner discovered" in line:
243+
logging.error(line.rstrip('\n'))
244+
handle_discovery_failure([linux_tv_app_log_path, linux_tv_casting_app_log_path])
245+
246+
# Look for 'Discovered Commissioner'.
247+
if "Discovered Commissioner" in line:
248+
valid_discovered_commissioner = line.rstrip('\n')
249+
250+
# Continue parsing the content that belongs to the "Discovered Commissioner".
251+
continue_parsing = True
252+
253+
# Initialize variables to store the information of interest.
254+
valid_vendor_id: Optional[str] = None
255+
valid_product_id: Optional[str] = None
256+
valid_device_type: Optional[str] = None
257+
258+
if continue_parsing:
259+
# Check if the Vendor ID, Product ID, and Device Type match the expected constant values.
260+
# If they do not match, then handle the discovery failure.
261+
if 'Vendor ID:' in line:
262+
valid_vendor_id = validate_value(VENDOR_ID, line, 'Vendor ID')
263+
264+
if not valid_vendor_id:
265+
handle_discovery_failure([linux_tv_app_log_path, linux_tv_casting_app_log_path])
266+
else:
267+
valid_vendor_id = line.rstrip('\n')
268+
269+
elif 'Product ID:' in line:
270+
valid_product_id = validate_value(PRODUCT_ID, line, 'Product ID')
271+
272+
if not valid_product_id:
273+
handle_discovery_failure([linux_tv_app_log_path, linux_tv_casting_app_log_path])
274+
else:
275+
valid_product_id = line.rstrip('\n')
276+
277+
elif 'Device Type:' in line:
278+
valid_device_type = validate_value(DEVICE_TYPE, line, 'Device Type')
279+
280+
if not valid_device_type:
281+
handle_discovery_failure([linux_tv_app_log_path, linux_tv_casting_app_log_path])
282+
else:
283+
valid_device_type = line.rstrip('\n')
284+
285+
# At this point, all values of interest are valid, so we stop parsing.
286+
continue_parsing = False
287+
288+
# Only a discovered commissioner that has valid vendor id, product id,
289+
# and device type will allow for 'Discovery success!'.
290+
if valid_vendor_id and valid_product_id and valid_device_type:
291+
logging.info('Found a valid commissioner in the Linux tv-casting-app logs:')
292+
logging.info(valid_discovered_commissioner)
293+
logging.info(valid_vendor_id)
294+
logging.info(valid_product_id)
295+
logging.info(valid_device_type)
296+
logging.info('Discovery success!')
297+
# pdb.set_trace() # SHAO
298+
return
299+
300+
301+
if __name__ == '__main__':
302+
303+
# Start with a clean slate by removing any previously cached entries.
304+
os.system('rm -f /tmp/chip_*')
305+
306+
# Test discovery between the Linux tv-casting-app and the tv-app.
307+
test_discovery_fn()

0 commit comments

Comments
 (0)