Skip to content

Commit dc5a683

Browse files
committedApr 24, 2024
Addressed @andy31415 PR comments - Updated constants to hex format, updated to follow doc-style comment format, removed redundant comments, removed LogFileManager class as temporary directory will handle file clean up, split test_discovery_fn to improve code readability.
1 parent ff254af commit dc5a683

File tree

1 file changed

+108
-189
lines changed

1 file changed

+108
-189
lines changed
 

‎scripts/tests/run_tv_casting_test.py

+108-189
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import sys
2121
import tempfile
2222
import time
23-
from typing import List, Optional
23+
from typing import List, Optional, TextIO, Tuple
2424

2525
import click
2626

@@ -36,264 +36,183 @@
3636

3737
# Values that identify the Linux tv-app and are noted in the 'Device Configuration' in the Linux tv-app output
3838
# as well as under the 'Discovered Commissioner' details in the Linux tv-casting-app output.
39-
VENDOR_ID = 65521 # Test vendor id
40-
PRODUCT_ID = 32769 # Test product id
41-
DEVICE_TYPE = 35 # Casting video player
42-
43-
44-
class LogFileManager:
45-
"""
46-
A context manager for managing log files.
47-
48-
This class provides a context manager for safely opening and closing log files.
49-
It ensures that log files are properly managed, allowing reading, writing, or
50-
both, depending on the specified mode.
51-
"""
52-
53-
# Initialize LogFileManager.
54-
# log_file_path (str): The path to the log file.
55-
# mode (str): The file mode for opening the log file (default is 'w+' for read/write mode).
56-
def __init__(self, log_file_path: str, mode: str = 'w+'):
57-
self.log_file_path = log_file_path
58-
self.mode = mode
59-
60-
# Enter the context manager to open and return the log file.
61-
def __enter__(self):
62-
try:
63-
self.file = open(self.log_file_path, self.mode)
64-
except FileNotFoundError:
65-
# Handle file not found error
66-
raise FileNotFoundError(f"Log file '{self.log_file_path}' not found.")
67-
except IOError:
68-
# Handle IO error
69-
raise IOError(f"Error opening log file '{self.log_file_path}'.")
70-
return self.file
71-
72-
# Exit the context manager, closing and removing the log file created.
73-
# exception_type: The type of exception that occurred, if any.
74-
# exception_value: The value of the exception, if any.
75-
# traceback: The traceback of the exception.
76-
def __exit__(self, exception_type, exception_value, traceback):
77-
self.file.close()
78-
79-
if os.path.exists(self.log_file_path):
80-
os.remove(self.log_file_path)
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
8142

8243

8344
class ProcessManager:
84-
"""
85-
A context manager for managing subprocesses.
45+
"""A context manager for managing subprocesses.
8646
8747
This class provides a context manager for safely starting and stopping a subprocess.
8848
"""
8949

90-
# Initialize ProcessManager.
91-
# command (list): The command to execute as a subprocess.
92-
# stdout (file): File-like object to which the subprocess's standard output will be redirected.
93-
# stderr (file): File-like object to which the subprocess's standard error will be redirected.
9450
def __init__(self, command: List[str], stdout, stderr):
9551
self.command = command
9652
self.stdout = stdout
9753
self.stderr = stderr
9854

99-
# Enter the context manager to start the subprocess and return it.
10055
def __enter__(self):
10156
self.process = subprocess.Popen(self.command, stdout=self.stdout, stderr=self.stderr, text=True)
10257
return self.process
10358

104-
# Exit the context manager, terminating the subprocess.
105-
# exception_type: The type of exception that occurred, if any.
106-
# exception_value: The value of the exception, if any.
107-
# traceback: The traceback of the exception.
10859
def __exit__(self, exception_type, exception_value, traceback):
10960
self.process.terminate()
11061
self.process.wait()
11162

11263

113-
# Dump the contents of a log file to the console.
114-
# log_file_path: The path to the log file.
115-
def dump_logs_to_console(log_file_path: str):
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."""
11666
print('\nDumping logs from: ', log_file_path)
11767

118-
with LogFileManager(log_file_path, 'r') as file:
68+
with open(log_file_path, 'r') as file:
11969
for line in file:
12070
print(line.rstrip())
12171

12272

123-
# Log 'Discovery failed!' as an error, dump the contents of the log files
124-
# to the console, exit on error.
125-
# log_file_paths: A list of paths to the log files, i.e. the path to the
126-
# Linux tv-casting-app logs and the tv-app logs.
12773
def handle_discovery_failure(log_file_paths: List[str]):
74+
"""Log 'Discovery failed!' as error, dump log files to console, exit on error."""
12875
logging.error('Discovery failed!')
12976

13077
for log_file_path in log_file_paths:
131-
dump_logs_to_console(log_file_path)
78+
try:
79+
dump_temporary_logs_to_console(log_file_path)
80+
except Exception as e:
81+
logging.exception(f"Failed to dump {log_file_path}: {e}")
13282

13383
sys.exit(1)
13484

13585

136-
# Extract and return an integer value from a given output string.
137-
# line: The string containing the integer value.
138-
#
139-
# The string is expected to be in the following format as it is received
140-
# from the Linux tv-casting-app output:
141-
# \x1b[0;34m[1713741926895] [7276:9521344] [DIS] Vendor ID: 65521\x1b[0m
142-
# The integer value to be extracted here is 65521.
14386
def extract_value_from_string(line: str) -> int:
87+
"""Extract and return integer value from given output string.
88+
89+
The string is expected to be in the following format as it is received
90+
from the Linux tv-casting-app output:
91+
\x1b[0;34m[1713741926895] [7276:9521344] [DIS] Vendor ID: 65521\x1b[0m
92+
The integer value to be extracted here is 65521.
93+
"""
14494
value = line.split(':')[-1].strip().replace('\x1b[0m', '')
14595
value = int(value)
14696

14797
return value
14898

14999

150-
# Validate if the discovered value matches the expected value.
151-
# expected_value: The expected integer value, i.e. any of the VENDOR_ID,
152-
# PRODUCT_ID, or DEVICE_TYPE constants.
153-
# line: The string containing the value of interest that will be compared
154-
# to the expected value.
155-
# value_name: The name of the discovered value, i.e. 'Vendor ID', 'Product ID',
156-
# or 'Device Type'.
157-
# Return False if the discovered value does not match, True otherwise.
158-
def validate_value(expected_value: int, line: str, value_name: str) -> bool:
159-
# Extract the integer value from the string.
100+
def validate_value(expected_value: int, log_paths: List[str], line: str, value_name: str) -> Optional[str]:
101+
"""Validate a value in a string against an expected value."""
160102
value = extract_value_from_string(line)
161103

162-
# If the discovered value does not match the expected value,
163-
# log the error and return False.
164104
if value != expected_value:
165105
logging.error(f'{value_name} does not match the expected value!')
166106
logging.error(f'Expected {value_name}: {expected_value}')
167107
logging.error(line.rstrip('\n'))
168-
return False
108+
handle_discovery_failure(log_paths)
109+
return None
110+
111+
# Return the line containing the valid value.
112+
return line.rstrip('\n')
113+
114+
115+
def start_up_tv_app_success(tv_app_process: subprocess.Popen, linux_tv_app_log_file: TextIO) -> bool:
116+
"""Check if the Linux tv-app is able to successfully start or until timeout occurs."""
117+
start_wait_time = time.time()
169118

170-
# Return True if the value matches the expected value.
171-
return True
119+
while True:
120+
# Check if the time elapsed since the start wait time exceeds the maximum allowed startup time for the TV app.
121+
if time.time() - start_wait_time > TV_APP_MAX_START_WAIT_SEC:
122+
logging.error("The Linux tv-app process did not start successfully within the timeout.")
123+
return False
124+
125+
tv_app_output_line = tv_app_process.stdout.readline()
126+
127+
linux_tv_app_log_file.write(tv_app_output_line)
128+
linux_tv_app_log_file.flush()
129+
130+
# Check if the Linux tv-app started successfully.
131+
if "Started commissioner" in tv_app_output_line:
132+
logging.info('Linux tv-app is up and running!')
133+
return True
134+
135+
136+
def parse_output_for_valid_commissioner(tv_casting_app_info: Tuple[subprocess.Popen, TextIO], log_paths: List[str]):
137+
"""Parse the output of the Linux tv-casting-app to find a valid commissioner."""
138+
tv_casting_app_process, linux_tv_casting_app_log_file = tv_casting_app_info
139+
140+
valid_discovered_commissioner = None
141+
valid_vendor_id = None
142+
valid_product_id = None
143+
valid_device_type = None
144+
145+
# Read the output as we receive it from the tv-casting-app subprocess.
146+
for line in tv_casting_app_process.stdout:
147+
linux_tv_casting_app_log_file.write(line)
148+
linux_tv_casting_app_log_file.flush()
149+
150+
# Fail fast if "No commissioner discovered" string found.
151+
if "No commissioner discovered" in line:
152+
logging.error(line.rstrip('\n'))
153+
handle_discovery_failure(log_paths)
154+
155+
elif "Discovered Commissioner" in line:
156+
valid_discovered_commissioner = line.rstrip('\n')
157+
158+
elif valid_discovered_commissioner:
159+
# Continue parsing the output for the information of interest under 'Discovered Commissioner'
160+
if 'Vendor ID:' in line:
161+
valid_vendor_id = validate_value(VENDOR_ID, log_paths, line, 'Vendor ID')
162+
163+
elif 'Product ID:' in line:
164+
valid_product_id = validate_value(PRODUCT_ID, log_paths, line, 'Product ID')
165+
166+
elif 'Device Type:' in line:
167+
valid_device_type = validate_value(DEVICE_TYPE_CASTING_VIDEO_PLAYER, log_paths, line, 'Device Type')
168+
169+
# A valid commissioner has VENDOR_ID, PRODUCT_ID, and DEVICE TYPE in its list of entries.
170+
if valid_vendor_id and valid_product_id and valid_device_type:
171+
logging.info('Found a valid commissioner in the Linux tv-casting-app logs:')
172+
logging.info(valid_discovered_commissioner)
173+
logging.info(valid_vendor_id)
174+
logging.info(valid_product_id)
175+
logging.info(valid_device_type)
176+
logging.info('Discovery success!')
177+
break
172178

173179

174-
# Test if the Linux tv-casting-app is able to discover the Linux tv-app. Both will
175-
# run separately as subprocesses, with their outputs written to respective log files.
176-
# Default paths for the executables are provided but can be overridden via command line
177-
# arguments. For example: python3 run_tv_casting_test.py --tv-app-rel-path=path/to/tv-app
178-
# --tv-casting-app-rel-path=path/to/tv-casting-app
179180
@click.command()
180181
@click.option('--tv-app-rel-path', type=str, default='out/tv-app/chip-tv-app', help='Path to the Linux tv-app executable.')
181182
@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.')
182183
def test_discovery_fn(tv_app_rel_path, tv_casting_app_rel_path):
184+
"""Test if the Linux tv-casting-app is able to discover the Linux tv-app.
183185
186+
Default paths for the executables are provided but can be overridden via command line arguments.
187+
For example: python3 run_tv_casting_test.py --tv-app-rel-path=path/to/tv-app
188+
--tv-casting-app-rel-path=path/to/tv-casting-app
189+
"""
184190
# Store the log files to a temporary directory.
185191
with tempfile.TemporaryDirectory() as temp_dir:
186192
linux_tv_app_log_path = os.path.join(temp_dir, LINUX_TV_APP_LOGS)
187193
linux_tv_casting_app_log_path = os.path.join(temp_dir, LINUX_TV_CASTING_APP_LOGS)
188194

189-
# Open and write to the log file for the Linux tv-app.
190-
with LogFileManager(linux_tv_app_log_path, 'w') as linux_tv_app_log_file:
191-
tv_app_abs_path = os.path.abspath(tv_app_rel_path)
195+
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:
192196

193197
# Configure command options to disable stdout buffering during tests.
194198
disable_stdout_buffering_cmd = []
195199
# On Unix-like systems, use stdbuf to disable stdout buffering.
196200
if sys.platform == 'darwin' or sys.platform == 'linux':
197201
disable_stdout_buffering_cmd = ['stdbuf', '-o0', '-i0']
198202

203+
tv_app_abs_path = os.path.abspath(tv_app_rel_path)
199204
# Run the Linux tv-app subprocess.
200205
with ProcessManager(disable_stdout_buffering_cmd + [tv_app_abs_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) as tv_app_process:
201-
start_wait_time = time.time()
202-
203-
# Loop until either the subprocess starts successfully or timeout occurs.
204-
while True:
205-
# Check if the time elapsed since the start wait time exceeds the maximum allowed startup time for the TV app.
206-
if time.time() - start_wait_time > TV_APP_MAX_START_WAIT_SEC:
207-
logging.error("The Linux tv-app process did not start successfully within the timeout.")
208-
handle_discovery_failure([linux_tv_app_log_path])
209-
210-
# Read one line of output at a time.
211-
tv_app_output_line = tv_app_process.stdout.readline()
212-
213-
# Write the output to the file.
214-
linux_tv_app_log_file.write(tv_app_output_line)
215-
linux_tv_app_log_file.flush()
216-
217-
# Check if the Linux tv-app started successfully.
218-
if "Started commissioner" in tv_app_output_line:
219-
logging.info('Linux tv-app is up and running!')
220-
221-
# If the string is found, then break out of the loop and go ahead with running the Linux tv-casting-app.
222-
break
223-
224-
# Open and write to the log file for the Linux tv-casting-app.
225-
with LogFileManager(linux_tv_casting_app_log_path, 'w') as linux_tv_casting_app_log_file:
226-
tv_casting_app_abs_path = os.path.abspath(tv_casting_app_rel_path)
227-
228-
# Run the Linux tv-casting-app subprocess.
229-
with ProcessManager(disable_stdout_buffering_cmd + [tv_casting_app_abs_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) as tv_casting_app_process:
230-
# Initialize variables.
231-
continue_parsing = False
232-
valid_discovered_commissioner = ''
233-
234-
# Read the output as we receive it from the tv-casting-app subprocess.
235-
for line in tv_casting_app_process.stdout:
236-
# Write to the Linux tv-casting-app log file.
237-
linux_tv_casting_app_log_file.write(line)
238-
linux_tv_casting_app_log_file.flush()
239-
240-
# Fail fast if "No commissioner discovered" string found.
241-
if "No commissioner discovered" in line:
242-
logging.error(line.rstrip('\n'))
243-
handle_discovery_failure([linux_tv_app_log_path, linux_tv_casting_app_log_path])
244-
245-
# Look for 'Discovered Commissioner'.
246-
if "Discovered Commissioner" in line:
247-
valid_discovered_commissioner = line.rstrip('\n')
248-
249-
# Continue parsing the content that belongs to the "Discovered Commissioner".
250-
continue_parsing = True
251-
252-
# Initialize variables to store the information of interest.
253-
valid_vendor_id: Optional[str] = None
254-
valid_product_id: Optional[str] = None
255-
valid_device_type: Optional[str] = None
256-
257-
if continue_parsing:
258-
# Check if the Vendor ID, Product ID, and Device Type match the expected constant values.
259-
# If they do not match, then handle the discovery failure.
260-
if 'Vendor ID:' in line:
261-
valid_vendor_id = validate_value(VENDOR_ID, line, 'Vendor ID')
262-
263-
if not valid_vendor_id:
264-
handle_discovery_failure([linux_tv_app_log_path, linux_tv_casting_app_log_path])
265-
else:
266-
valid_vendor_id = line.rstrip('\n')
267-
268-
elif 'Product ID:' in line:
269-
valid_product_id = validate_value(PRODUCT_ID, line, 'Product ID')
270-
271-
if not valid_product_id:
272-
handle_discovery_failure([linux_tv_app_log_path, linux_tv_casting_app_log_path])
273-
else:
274-
valid_product_id = line.rstrip('\n')
275-
276-
elif 'Device Type:' in line:
277-
valid_device_type = validate_value(DEVICE_TYPE, line, 'Device Type')
278-
279-
if not valid_device_type:
280-
handle_discovery_failure([linux_tv_app_log_path, linux_tv_casting_app_log_path])
281-
else:
282-
valid_device_type = line.rstrip('\n')
283-
284-
# At this point, all values of interest are valid, so we stop parsing.
285-
continue_parsing = False
286-
287-
# Only a discovered commissioner that has valid vendor id, product id,
288-
# and device type will allow for 'Discovery success!'.
289-
if valid_vendor_id and valid_product_id and valid_device_type:
290-
logging.info('Found a valid commissioner in the Linux tv-casting-app logs:')
291-
logging.info(valid_discovered_commissioner)
292-
logging.info(valid_vendor_id)
293-
logging.info(valid_product_id)
294-
logging.info(valid_device_type)
295-
logging.info('Discovery success!')
296-
return
206+
207+
if not start_up_tv_app_success(tv_app_process, linux_tv_app_log_file):
208+
handle_discovery_failure([linux_tv_app_log_path])
209+
210+
tv_casting_app_abs_path = os.path.abspath(tv_casting_app_rel_path)
211+
# Run the Linux tv-casting-app subprocess.
212+
with ProcessManager(disable_stdout_buffering_cmd + [tv_casting_app_abs_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) as tv_casting_app_process:
213+
log_paths = [linux_tv_app_log_path, linux_tv_casting_app_log_path]
214+
tv_casting_app_info = (tv_casting_app_process, linux_tv_casting_app_log_file)
215+
parse_output_for_valid_commissioner(tv_casting_app_info, log_paths)
297216

298217

299218
if __name__ == '__main__':

0 commit comments

Comments
 (0)