20
20
import sys
21
21
import tempfile
22
22
import time
23
- from typing import List , Optional , TextIO , Tuple
23
+ from typing import List , Optional , TextIO , Tuple , Union
24
24
25
25
import click
26
26
30
30
# The maximum amount of time to wait for the Linux tv-app to start before timeout.
31
31
TV_APP_MAX_START_WAIT_SEC = 2
32
32
33
+ # The maximum amount of time to commission the Linux tv-casting-app and the tv-app before timeout.
34
+ COMMISSIONING_STAGE_MAX_WAIT_SEC = 10
35
+
33
36
# File names of logs for the Linux tv-casting-app and the Linux tv-app.
34
37
LINUX_TV_APP_LOGS = 'Linux-tv-app-logs.txt'
35
38
LINUX_TV_CASTING_APP_LOGS = 'Linux-tv-casting-app-logs.txt'
@@ -47,13 +50,14 @@ class ProcessManager:
47
50
This class provides a context manager for safely starting and stopping a subprocess.
48
51
"""
49
52
50
- def __init__ (self , command : List [str ], stdout , stderr ):
53
+ def __init__ (self , command : List [str ], stdin , stdout , stderr ):
51
54
self .command = command
55
+ self .stdin = stdin
52
56
self .stdout = stdout
53
57
self .stderr = stderr
54
58
55
59
def __enter__ (self ):
56
- self .process = subprocess .Popen (self .command , stdout = self .stdout , stderr = self .stderr , text = True )
60
+ self .process = subprocess .Popen (self .command , stdin = self . stdin , stdout = self .stdout , stderr = self .stderr , text = True )
57
61
return self .process
58
62
59
63
def __exit__ (self , exception_type , exception_value , traceback ):
@@ -71,9 +75,9 @@ def dump_temporary_logs_to_console(log_file_path: str):
71
75
print (line .rstrip ())
72
76
73
77
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!' )
78
+ def handle_casting_failure ( casting_state : str , log_file_paths : List [str ]):
79
+ """Log '{casting_state} failed!' as error, dump log files to console, exit on error."""
80
+ logging .error (casting_state + ' failed!' )
77
81
78
82
for log_file_path in log_file_paths :
79
83
try :
@@ -84,30 +88,34 @@ def handle_discovery_failure(log_file_paths: List[str]):
84
88
sys .exit (1 )
85
89
86
90
87
- def extract_value_from_string (line : str ) -> int :
88
- """Extract and return integer value from given output string.
91
+ def extract_value_from_string (line : str ) -> str :
92
+ """Extract and return value from given input string.
89
93
90
94
The string is expected to be in the following format as it is received
91
95
from the Linux tv-casting-app output:
92
96
\x1b [0;34m[1713741926895] [7276:9521344] [DIS] Vendor ID: 65521\x1b [0m
93
97
The integer value to be extracted here is 65521.
98
+ Or:
99
+ \x1b [0;34m[1714583616179] [7029:2386956] [SVR] device Name: Test TV casting app\x1b [0m
100
+ The substring to be extracted here is 'Test TV casting app'.
94
101
"""
95
102
value = line .split (':' )[- 1 ].strip ().replace ('\x1b [0m' , '' )
96
- value = int (value )
97
103
98
104
return value
99
105
100
106
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."""
107
+ def validate_value (casting_state : str , expected_value : Union [ str , int ] , log_paths : List [str ], line : str , value_name : str ) -> Optional [str ]:
108
+ """Validate a value in a string against an expected value during a given casting state ."""
103
109
value = extract_value_from_string (line )
104
110
111
+ if isinstance (expected_value , int ):
112
+ value = int (value )
113
+
105
114
if value != expected_value :
106
115
logging .error (f'{ value_name } does not match the expected value!' )
107
116
logging .error (f'Expected { value_name } : { expected_value } ' )
108
117
logging .error (line .rstrip ('\n ' ))
109
- handle_discovery_failure (log_paths )
110
- return None
118
+ handle_casting_failure (casting_state , log_paths )
111
119
112
120
# Return the line containing the valid value.
113
121
return line .rstrip ('\n ' )
@@ -120,7 +128,7 @@ def start_up_tv_app_success(tv_app_process: subprocess.Popen, linux_tv_app_log_f
120
128
while True :
121
129
# Check if the time elapsed since the start wait time exceeds the maximum allowed startup time for the TV app.
122
130
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." )
131
+ logging .error (' The Linux tv-app process did not start successfully within the timeout.' )
124
132
return False
125
133
126
134
tv_app_output_line = tv_app_process .stdout .readline ()
@@ -134,7 +142,172 @@ def start_up_tv_app_success(tv_app_process: subprocess.Popen, linux_tv_app_log_f
134
142
return True
135
143
136
144
137
- def parse_output_for_valid_commissioner (tv_casting_app_info : Tuple [subprocess .Popen , TextIO ], log_paths : List [str ]):
145
+ def initiate_cast_request_success (tv_casting_app_info : Tuple [subprocess .Popen , TextIO ], valid_discovered_commissioner_number : str ) -> bool :
146
+ """Initiate commissioning between Linux tv-casting-app and tv-app by sending `cast request {valid_discovered_commissioner_number}` via Linux tv-casting-app process."""
147
+ tv_casting_app_process , linux_tv_casting_app_log_file = tv_casting_app_info
148
+
149
+ start_wait_time = time .time ()
150
+
151
+ while True :
152
+ # Check if we exceeded the maximum wait time for initiating 'cast request' from the Linux tv-casting-app to the Linux tv-app.
153
+ if time .time () - start_wait_time > COMMISSIONING_STAGE_MAX_WAIT_SEC :
154
+ logging .error ('The command `cast request ' + valid_discovered_commissioner_number +
155
+ '` was not issued to the Linux tv-casting-app process within the timeout.' )
156
+ return False
157
+
158
+ tv_casting_app_output_line = tv_casting_app_process .stdout .readline ()
159
+ if tv_casting_app_output_line :
160
+ linux_tv_casting_app_log_file .write (tv_casting_app_output_line )
161
+ linux_tv_casting_app_log_file .flush ()
162
+
163
+ if 'cast request 0' in tv_casting_app_output_line :
164
+ tv_casting_app_process .stdin .write ('cast request ' + valid_discovered_commissioner_number + '\n ' )
165
+ tv_casting_app_process .stdin .flush ()
166
+ # Move to the next line otherwise we will keep entering this code block
167
+ next_line = tv_casting_app_process .stdout .readline ()
168
+ linux_tv_casting_app_log_file .write (next_line )
169
+ linux_tv_casting_app_log_file .flush ()
170
+ logging .info ('Sent `' + next_line .rstrip ('\n ' ) + '` to the Linux tv-casting-app process.' )
171
+ return True
172
+
173
+
174
+ def extract_device_info_from_tv_casting_app (tv_casting_app_info : Tuple [subprocess .Popen , TextIO ]) -> Tuple [Optional [str ], Optional [int ], Optional [int ]]:
175
+ """Extract device information from the 'Identification Declaration' block in the Linux tv-casting-app output."""
176
+ tv_casting_app_process , linux_tv_casting_app_log_file = tv_casting_app_info
177
+
178
+ device_name = None
179
+ vendor_id = None
180
+ product_id = None
181
+
182
+ for line in tv_casting_app_process .stdout :
183
+ linux_tv_casting_app_log_file .write (line )
184
+ linux_tv_casting_app_log_file .flush ()
185
+
186
+ if 'device Name' in line :
187
+ device_name = extract_value_from_string (line )
188
+ elif 'vendor id' in line :
189
+ vendor_id = extract_value_from_string (line )
190
+ vendor_id = int (vendor_id )
191
+ elif 'product id' in line :
192
+ product_id = extract_value_from_string (line )
193
+ product_id = int (product_id )
194
+
195
+ if device_name and vendor_id and product_id :
196
+ break
197
+
198
+ return device_name , vendor_id , product_id
199
+
200
+
201
+ def validate_identification_declaration_message_on_tv_app (tv_app_info : Tuple [subprocess .Popen , TextIO ], expected_device_name : str , expected_vendor_id : int , expected_product_id : int , log_paths : List [str ]) -> bool :
202
+ """Validate device information from the 'Identification Declaration' block from the Linux tv-app output against the expected values."""
203
+ tv_app_process , linux_tv_app_log_file = tv_app_info
204
+
205
+ parsing_identification_block = False
206
+ start_wait_time = time .time ()
207
+
208
+ while True :
209
+ # Check if we exceeded the maximum wait time for validating the device information from the Linux tv-app to the corresponding values from the Linux tv-app.
210
+ if time .time () - start_wait_time > COMMISSIONING_STAGE_MAX_WAIT_SEC :
211
+ logging .erro ('The device information from the Linux tv-app output was not validated against the corresponding values from the Linux tv-casting-app output within the timeout.' )
212
+ return False
213
+
214
+ tv_app_line = tv_app_process .stdout .readline ()
215
+
216
+ if tv_app_line :
217
+ linux_tv_app_log_file .write (tv_app_line )
218
+ linux_tv_app_log_file .flush ()
219
+
220
+ if 'Identification Declaration Start' in tv_app_line :
221
+ logging .info ('"Identification Declaration" block from the Linux tv-app output:' )
222
+ logging .info (tv_app_line .rstrip ('\n ' ))
223
+ parsing_identification_block = True
224
+ elif parsing_identification_block :
225
+ logging .info (tv_app_line .rstrip ('\n ' ))
226
+ if 'device Name' in tv_app_line :
227
+ validate_value ('Commissioning' , expected_device_name , log_paths , tv_app_line , 'device Name' )
228
+ elif 'vendor id' in tv_app_line :
229
+ validate_value ('Commissioning' , expected_vendor_id , log_paths , tv_app_line , 'vendor id' )
230
+ elif 'product id' in tv_app_line :
231
+ validate_value ('Commissioning' , expected_product_id , log_paths , tv_app_line , 'product id' )
232
+ elif 'Identification Declaration End' in tv_app_line :
233
+ parsing_identification_block = False
234
+ return True
235
+
236
+
237
+ def validate_tv_casting_request_approval (tv_app_info : Tuple [subprocess .Popen , TextIO ], log_paths : List [str ]) -> bool :
238
+ """Validate that the TV casting request from the Linux tv-casting-app to the Linux tv-app is approved by sending `controller ux ok` via Linux tv-app process."""
239
+ tv_app_process , linux_tv_app_log_file = tv_app_info
240
+
241
+ start_wait_time = time .time ()
242
+
243
+ while True :
244
+ # Check if we exceeded the maximum wait time for sending 'controller ux ok' from the Linux tv-app to the Linux tv-casting-app.
245
+ if time .time () - start_wait_time > COMMISSIONING_STAGE_MAX_WAIT_SEC :
246
+ logging .error ('The cast request from the Linux tv-casting-app to the Linux tv-app was not approved within the timeout.' )
247
+ return False
248
+
249
+ tv_app_line = tv_app_process .stdout .readline ()
250
+
251
+ if tv_app_line :
252
+ linux_tv_app_log_file .write (tv_app_line )
253
+ linux_tv_app_log_file .flush ()
254
+
255
+ if 'PROMPT USER: Test TV casting app is requesting permission to cast to this TV, approve?' in tv_app_line :
256
+ logging .info (tv_app_line .rstrip ('\n ' ))
257
+ elif 'Via Shell Enter: controller ux ok|cancel' in tv_app_line :
258
+ logging .info (tv_app_line .rstrip ('\n ' ))
259
+
260
+ tv_app_process .stdin .write ('controller ux ok\n ' )
261
+ tv_app_process .stdin .flush ()
262
+
263
+ tv_app_line = tv_app_process .stdout .readline ()
264
+ linux_tv_app_log_file .write (tv_app_line )
265
+ linux_tv_app_log_file .flush ()
266
+
267
+ logging .info ('Sent `' + tv_app_line .rstrip ('\n ' ) + '` to the Linux tv-app process.' )
268
+ return True
269
+
270
+
271
+ def validate_commissioning_success (tv_casting_app_info : Tuple [subprocess .Popen , TextIO ], tv_app_info : Tuple [subprocess .Popen , TextIO ], log_paths : List [str ]) -> bool :
272
+ """Parse output of Linux tv-casting-app and Linux tv-app output for strings indicating commissioning status."""
273
+ tv_casting_app_process , linux_tv_casting_app_log_file = tv_casting_app_info
274
+ tv_app_process , linux_tv_app_log_file = tv_app_info
275
+
276
+ start_wait_time = time .time ()
277
+
278
+ while True :
279
+ # Check if we exceeded the maximum wait time for validating commissioning success between the Linux tv-casting-app and the Linux tv-app.
280
+ if time .time () - start_wait_time > COMMISSIONING_STAGE_MAX_WAIT_SEC :
281
+ logging .error (
282
+ 'The commissioning between the Linux tv-casting-app process and the Linux tv-app process did not complete successfully within the timeout.' )
283
+ return False
284
+
285
+ tv_casting_line = tv_casting_app_process .stdout .readline ()
286
+ tv_app_line = tv_app_process .stdout .readline ()
287
+
288
+ if tv_casting_line :
289
+ linux_tv_casting_app_log_file .write (tv_casting_line )
290
+ linux_tv_casting_app_log_file .flush ()
291
+
292
+ if 'Commissioning completed successfully' in tv_casting_line :
293
+ logging .info ('Commissioning success noted on the Linux tv-casting-app output:' )
294
+ logging .info (tv_casting_line .rstrip ('\n ' ))
295
+ elif 'Commissioning failed' in tv_casting_line :
296
+ logging .error ('Commissioning failed noted on the Linux tv-casting-app output:' )
297
+ logging .error (tv_casting_line .rstrip ('\n ' ))
298
+ return False
299
+
300
+ if tv_app_line :
301
+ linux_tv_app_log_file .write (tv_app_line )
302
+ linux_tv_app_log_file .flush ()
303
+
304
+ if 'PROMPT USER: commissioning success' in tv_app_line :
305
+ logging .info ('Commissioning success noted on the Linux tv-app output:' )
306
+ logging .info (tv_app_line .rstrip ('\n ' ))
307
+ return True
308
+
309
+
310
+ def test_discovery_fn (tv_casting_app_info : Tuple [subprocess .Popen , TextIO ], log_paths : List [str ]) -> Optional [str ]:
138
311
"""Parse the output of the Linux tv-casting-app to find a valid commissioner."""
139
312
tv_casting_app_process , linux_tv_casting_app_log_file = tv_casting_app_info
140
313
@@ -151,21 +324,21 @@ def parse_output_for_valid_commissioner(tv_casting_app_info: Tuple[subprocess.Po
151
324
# Fail fast if "No commissioner discovered" string found.
152
325
if "No commissioner discovered" in line :
153
326
logging .error (line .rstrip ('\n ' ))
154
- handle_discovery_failure ( log_paths )
327
+ handle_casting_failure ( 'Discovery' , log_paths )
155
328
156
329
elif "Discovered Commissioner" in line :
157
330
valid_discovered_commissioner = line .rstrip ('\n ' )
158
331
159
332
elif valid_discovered_commissioner :
160
333
# Continue parsing the output for the information of interest under 'Discovered Commissioner'
161
334
if 'Vendor ID:' in line :
162
- valid_vendor_id = validate_value (VENDOR_ID , log_paths , line , 'Vendor ID' )
335
+ valid_vendor_id = validate_value ('Discovery' , VENDOR_ID , log_paths , line , 'Vendor ID' )
163
336
164
337
elif 'Product ID:' in line :
165
- valid_product_id = validate_value (PRODUCT_ID , log_paths , line , 'Product ID' )
338
+ valid_product_id = validate_value ('Discovery' , PRODUCT_ID , log_paths , line , 'Product ID' )
166
339
167
340
elif 'Device Type:' in line :
168
- valid_device_type = validate_value (DEVICE_TYPE_CASTING_VIDEO_PLAYER , log_paths , line , 'Device Type' )
341
+ valid_device_type = validate_value ('Discovery' , DEVICE_TYPE_CASTING_VIDEO_PLAYER , log_paths , line , 'Device Type' )
169
342
170
343
# A valid commissioner has VENDOR_ID, PRODUCT_ID, and DEVICE TYPE in its list of entries.
171
344
if valid_vendor_id and valid_product_id and valid_device_type :
@@ -177,12 +350,33 @@ def parse_output_for_valid_commissioner(tv_casting_app_info: Tuple[subprocess.Po
177
350
logging .info ('Discovery success!' )
178
351
break
179
352
353
+ return valid_discovered_commissioner
354
+
355
+
356
+ def test_commissioning_fn (valid_discovered_commissioner_number , tv_casting_app_info : Tuple [subprocess .Popen , TextIO ], tv_app_info : Tuple [subprocess .Popen , TextIO ], log_paths : List [str ]):
357
+ """Test commissioning between Linux tv-casting-app and Linux tv-app."""
358
+
359
+ if not initiate_cast_request_success (tv_casting_app_info , valid_discovered_commissioner_number ):
360
+ handle_casting_failure ('Commissioning' , log_paths )
361
+
362
+ # Extract the values from the 'Identification Declaration' block in the tv-casting-app output that we want to validate against.
363
+ expected_device_name , expected_vendor_id , expected_product_id = extract_device_info_from_tv_casting_app (tv_casting_app_info )
364
+
365
+ if not validate_identification_declaration_message_on_tv_app (tv_app_info , expected_device_name , expected_vendor_id , expected_product_id , log_paths ):
366
+ handle_casting_failure ('Commissioning' , log_paths )
367
+
368
+ if not validate_tv_casting_request_approval (tv_app_info , log_paths ):
369
+ handle_casting_failure ('Commissioning' , log_paths )
370
+
371
+ if not validate_commissioning_success (tv_casting_app_info , tv_app_info , log_paths ):
372
+ handle_casting_failure ('Commissioning' , log_paths )
373
+
180
374
181
375
@click .command ()
182
376
@click .option ('--tv-app-rel-path' , type = str , default = 'out/tv-app/chip-tv-app' , help = 'Path to the Linux tv-app executable.' )
183
377
@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.
378
+ def test_casting_fn (tv_app_rel_path , tv_casting_app_rel_path ):
379
+ """Test if the Linux tv-casting-app is able to discover and commission the Linux tv-app as part of casting .
186
380
187
381
Default paths for the executables are provided but can be overridden via command line arguments.
188
382
For example: python3 run_tv_casting_test.py --tv-app-rel-path=path/to/tv-app
@@ -203,23 +397,34 @@ def test_discovery_fn(tv_app_rel_path, tv_casting_app_rel_path):
203
397
204
398
tv_app_abs_path = os .path .abspath (tv_app_rel_path )
205
399
# 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 :
400
+ with ProcessManager (disable_stdout_buffering_cmd + [tv_app_abs_path ], stdin = subprocess . PIPE , stdout = subprocess .PIPE , stderr = subprocess .PIPE ) as tv_app_process :
207
401
208
402
if not start_up_tv_app_success (tv_app_process , linux_tv_app_log_file ):
209
- handle_discovery_failure ( [linux_tv_app_log_path ])
403
+ handle_casting_failure ( 'Discovery' , [linux_tv_app_log_path ])
210
404
211
405
tv_casting_app_abs_path = os .path .abspath (tv_casting_app_rel_path )
212
406
# 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 :
407
+ with ProcessManager (disable_stdout_buffering_cmd + [tv_casting_app_abs_path ], stdin = subprocess . PIPE , stdout = subprocess .PIPE , stderr = subprocess .PIPE ) as tv_casting_app_process :
214
408
log_paths = [linux_tv_app_log_path , linux_tv_casting_app_log_path ]
215
409
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 )
410
+ tv_app_info = (tv_app_process , linux_tv_app_log_file )
411
+ valid_discovered_commissioner = test_discovery_fn (tv_casting_app_info , log_paths )
412
+
413
+ if not valid_discovered_commissioner :
414
+ handle_casting_failure ('Discovery' , log_paths )
415
+
416
+ # We need the valid discovered commissioner number to continue with commissioning.
417
+ # Example string: \x1b[0;32m[1714582264602] [77989:2286038] [SVR] Discovered Commissioner #0\x1b[0m
418
+ # The value '0' will be extracted from the string.
419
+ valid_discovered_commissioner_number = valid_discovered_commissioner .split ('#' )[- 1 ].replace ('\x1b [0m' , '' )
420
+
421
+ test_commissioning_fn (valid_discovered_commissioner_number , tv_casting_app_info , tv_app_info , log_paths )
217
422
218
423
219
424
if __name__ == '__main__' :
220
425
221
426
# Start with a clean slate by removing any previously cached entries.
222
427
os .system ('rm -f /tmp/chip_*' )
223
428
224
- # Test discovery between the Linux tv-casting-app and the tv-app.
225
- test_discovery_fn ()
429
+ # Test casting ( discovery and commissioning) between the Linux tv-casting-app and the tv-app.
430
+ test_casting_fn ()
0 commit comments