|
20 | 20 | import sys
|
21 | 21 | import tempfile
|
22 | 22 | import time
|
23 |
| -from typing import List, Optional |
| 23 | +from typing import List, Optional, TextIO, Tuple |
24 | 24 |
|
25 | 25 | import click
|
26 | 26 |
|
|
36 | 36 |
|
37 | 37 | # Values that identify the Linux tv-app and are noted in the 'Device Configuration' in the Linux tv-app output
|
38 | 38 | # 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 |
81 | 42 |
|
82 | 43 |
|
83 | 44 | class ProcessManager:
|
84 |
| - """ |
85 |
| - A context manager for managing subprocesses. |
| 45 | + """A context manager for managing subprocesses. |
86 | 46 |
|
87 | 47 | This class provides a context manager for safely starting and stopping a subprocess.
|
88 | 48 | """
|
89 | 49 |
|
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. |
94 | 50 | def __init__(self, command: List[str], stdout, stderr):
|
95 | 51 | self.command = command
|
96 | 52 | self.stdout = stdout
|
97 | 53 | self.stderr = stderr
|
98 | 54 |
|
99 |
| - # Enter the context manager to start the subprocess and return it. |
100 | 55 | def __enter__(self):
|
101 | 56 | self.process = subprocess.Popen(self.command, stdout=self.stdout, stderr=self.stderr, text=True)
|
102 | 57 | return self.process
|
103 | 58 |
|
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. |
108 | 59 | def __exit__(self, exception_type, exception_value, traceback):
|
109 | 60 | self.process.terminate()
|
110 | 61 | self.process.wait()
|
111 | 62 |
|
112 | 63 |
|
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.""" |
116 | 66 | print('\nDumping logs from: ', log_file_path)
|
117 | 67 |
|
118 |
| - with LogFileManager(log_file_path, 'r') as file: |
| 68 | + with open(log_file_path, 'r') as file: |
119 | 69 | for line in file:
|
120 | 70 | print(line.rstrip())
|
121 | 71 |
|
122 | 72 |
|
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. |
127 | 73 | def handle_discovery_failure(log_file_paths: List[str]):
|
| 74 | + """Log 'Discovery failed!' as error, dump log files to console, exit on error.""" |
128 | 75 | logging.error('Discovery failed!')
|
129 | 76 |
|
130 | 77 | 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}") |
132 | 82 |
|
133 | 83 | sys.exit(1)
|
134 | 84 |
|
135 | 85 |
|
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. |
143 | 86 | 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 | + """ |
144 | 94 | value = line.split(':')[-1].strip().replace('\x1b[0m', '')
|
145 | 95 | value = int(value)
|
146 | 96 |
|
147 | 97 | return value
|
148 | 98 |
|
149 | 99 |
|
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.""" |
160 | 102 | value = extract_value_from_string(line)
|
161 | 103 |
|
162 |
| - # If the discovered value does not match the expected value, |
163 |
| - # log the error and return False. |
164 | 104 | if value != expected_value:
|
165 | 105 | logging.error(f'{value_name} does not match the expected value!')
|
166 | 106 | logging.error(f'Expected {value_name}: {expected_value}')
|
167 | 107 | 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() |
169 | 118 |
|
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 |
172 | 178 |
|
173 | 179 |
|
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 |
179 | 180 | @click.command()
|
180 | 181 | @click.option('--tv-app-rel-path', type=str, default='out/tv-app/chip-tv-app', help='Path to the Linux tv-app executable.')
|
181 | 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.')
|
182 | 183 | 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. |
183 | 185 |
|
| 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 | + """ |
184 | 190 | # Store the log files to a temporary directory.
|
185 | 191 | with tempfile.TemporaryDirectory() as temp_dir:
|
186 | 192 | linux_tv_app_log_path = os.path.join(temp_dir, LINUX_TV_APP_LOGS)
|
187 | 193 | linux_tv_casting_app_log_path = os.path.join(temp_dir, LINUX_TV_CASTING_APP_LOGS)
|
188 | 194 |
|
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: |
192 | 196 |
|
193 | 197 | # Configure command options to disable stdout buffering during tests.
|
194 | 198 | disable_stdout_buffering_cmd = []
|
195 | 199 | # On Unix-like systems, use stdbuf to disable stdout buffering.
|
196 | 200 | if sys.platform == 'darwin' or sys.platform == 'linux':
|
197 | 201 | disable_stdout_buffering_cmd = ['stdbuf', '-o0', '-i0']
|
198 | 202 |
|
| 203 | + tv_app_abs_path = os.path.abspath(tv_app_rel_path) |
199 | 204 | # Run the Linux tv-app subprocess.
|
200 | 205 | 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) |
297 | 216 |
|
298 | 217 |
|
299 | 218 | if __name__ == '__main__':
|
|
0 commit comments