]>
git.ipfire.org Git - thirdparty/hostap.git/blob - tests/hwsim/vm/parallel-vm.py
3dd09584f8bcdb91756fbadf1eb542a3e81d329d
3 # Parallel VM test case executor
4 # Copyright (c) 2014-2019, Jouni Malinen <j@w1.fi>
6 # This software may be distributed under the terms of the BSD license.
7 # See README for more details.
9 from __future__
import print_function
13 import multiprocessing
21 logger
= logging
.getLogger()
23 # Test cases that take significantly longer time to execute than average.
24 long_tests
= ["ap_roam_open",
25 "wpas_mesh_password_mismatch_retry",
26 "wpas_mesh_password_mismatch",
27 "hostapd_oom_wpa2_psk_connect",
28 "ap_hs20_fetch_osu_stop",
31 "nfc_wps_er_handover_pk_hash_mismatch_sta",
32 "go_neg_peers_force_diff_freq",
35 "ap_pmf_sta_unprot_deauth_burst",
36 "ap_bss_add_remove_during_ht_scan",
38 "autoscan_exponential",
41 "ap_inactivity_disconnect",
42 "scan_bss_expiration_age",
44 "discovery_group_client",
50 "ap_ht_40mhz_intolerant_ap",
52 "discovery_pd_retries",
53 "ap_wps_setup_locked_timeout",
61 "dfs_radar_vht80_downgrade",
63 "grpform_cred_ready_timeout",
64 "hostapd_oom_wpa2_eap_connect",
67 "hostapd_oom_wpa2_eap",
69 "proxyarp_open_ebtables",
70 "proxyarp_open_ebtables_ipv6",
72 "obss_scan_40_intolerant",
77 "ap_wps_iteration_error",
79 "ap_wps_http_timeout",
80 "p2p_go_move_reg_change",
83 "p2p_go_move_scm_peer_supports",
84 "p2p_go_move_scm_peer_does_not_support",
85 "p2p_go_move_scm_multi"]
89 for i
in range(num_servers
):
90 failed
+= vm
[i
]['failed']
93 def vm_read_stdout(vm
, test_queue
):
94 global total_started
, total_passed
, total_failed
, total_skipped
96 global first_run_failures
100 out
= vm
['proc'].stdout
.read()
105 if e
.errno
== errno
.EAGAIN
:
108 logger
.debug("VM[%d] stdout.read[%s]" % (vm
['idx'], out
.rstrip()))
109 pending
= vm
['pending'] + out
112 pos
= pending
.find('\n')
115 line
= pending
[0:pos
].rstrip()
116 pending
= pending
[(pos
+ 1):]
117 logger
.debug("VM[%d] stdout full line[%s]" % (vm
['idx'], line
))
118 if line
.startswith("READY"):
119 vm
['starting'] = False
122 elif line
.startswith("PASS"):
125 elif line
.startswith("FAIL"):
128 vals
= line
.split(' ')
130 logger
.info("VM[%d] incomplete FAIL line: %s" % (vm
['idx'],
135 logger
.debug("VM[%d] test case failed: %s" % (vm
['idx'], name
))
136 vm
['failed'].append(name
)
137 if name
!= vm
['current_name']:
138 logger
.info("VM[%d] test result mismatch: %s (expected %s)" % (vm
['idx'], name
, vm
['current_name']))
140 count
= vm
['current_count']
142 first_run_failures
.append(name
)
143 if rerun_failures
and count
< 1:
144 logger
.debug("Requeue test case %s" % name
)
145 test_queue
.append((name
, vm
['current_count'] + 1))
146 elif line
.startswith("NOT-FOUND"):
149 logger
.info("VM[%d] test case not found" % vm
['idx'])
150 elif line
.startswith("SKIP"):
153 elif line
.startswith("REASON"):
154 vm
['skip_reason'].append(line
[7:])
155 elif line
.startswith("START"):
157 if len(vm
['failed']) == 0:
158 vals
= line
.split(' ')
160 vm
['fail_seq'].append(vals
[1])
161 vm
['out'] += line
+ '\n'
163 vm
['pending'] = pending
166 def start_vm(vm
, sel
):
167 logger
.info("VM[%d] starting up" % (vm
['idx'] + 1))
168 vm
['starting'] = True
169 vm
['proc'] = subprocess
.Popen(vm
['cmd'],
170 stdin
=subprocess
.PIPE
,
171 stdout
=subprocess
.PIPE
,
172 stderr
=subprocess
.PIPE
)
174 for stream
in [vm
['proc'].stdout
, vm
['proc'].stderr
]:
176 fl
= fcntl
.fcntl(fd
, fcntl
.F_GETFL
)
177 fcntl
.fcntl(fd
, fcntl
.F_SETFL
, fl | os
.O_NONBLOCK
)
178 sel
.register(stream
, selectors
.EVENT_READ
, vm
)
180 def num_vm_starting():
182 for i
in range(num_servers
):
183 if vm
[i
]['starting']:
187 def vm_read_stderr(vm
):
189 err
= vm
['proc'].stderr
.read()
194 logger
.info("VM[%d] stderr.read[%s]" % (vm
['idx'], err
))
196 if e
.errno
!= errno
.EAGAIN
:
199 def vm_next_step(_vm
, scr
, test_queue
):
200 scr
.move(_vm
['idx'] + 1, 10)
203 _vm
['proc'].stdin
.write(b
'\n')
204 _vm
['proc'].stdin
.flush()
205 scr
.addstr("shutting down")
206 logger
.info("VM[%d] shutting down" % _vm
['idx'])
208 (name
, count
) = test_queue
.pop(0)
209 _vm
['current_name'] = name
210 _vm
['current_count'] = count
211 _vm
['proc'].stdin
.write(name
.encode() + b
'\n')
212 _vm
['proc'].stdin
.flush()
214 logger
.debug("VM[%d] start test %s" % (_vm
['idx'], name
))
216 def check_vm_start(scr
, sel
, test_queue
):
218 for i
in range(num_servers
):
223 # Either not yet started or already stopped VM
224 max_start
= multiprocessing
.cpu_count()
227 num_starting
= num_vm_starting()
228 if vm
[i
]['cmd'] and len(test_queue
) > num_starting
and \
229 num_starting
< max_start
:
232 scr
.addstr(i
+ 1, 10, "starting VM")
236 return running
, False
238 def vm_terminated(_vm
, scr
, sel
, test_queue
):
240 for stream
in [_vm
['proc'].stdout
, _vm
['proc'].stderr
]:
241 sel
.unregister(stream
)
243 scr
.move(_vm
['idx'] + 1, 10)
245 log
= '{}/{}.srv.{}/console'.format(dir, timestamp
, _vm
['idx'] + 1)
246 with
open(log
, 'r') as f
:
247 if "Kernel panic" in f
.read():
248 scr
.addstr("kernel panic")
249 logger
.info("VM[%d] kernel panic" % _vm
['idx'])
253 for i
in range(num_servers
):
256 if len(test_queue
) > num_vm
:
257 scr
.addstr("unexpected exit")
258 logger
.info("VM[%d] unexpected exit" % i
)
262 def update_screen(scr
, total_tests
):
263 scr
.move(num_servers
+ 1, 10)
265 scr
.addstr("{} %".format(int(100.0 * (total_passed
+ total_failed
+ total_skipped
) / total_tests
)))
266 scr
.addstr(num_servers
+ 1, 20,
267 "TOTAL={} STARTED={} PASS={} FAIL={} SKIP={}".format(total_tests
, total_started
, total_passed
, total_failed
, total_skipped
))
268 failed
= get_failed(vm
)
270 scr
.move(num_servers
+ 2, 0)
272 scr
.addstr("Failed test cases: ")
284 def show_progress(scr
):
290 global first_run_failures
291 global total_started
, total_passed
, total_failed
, total_skipped
292 global rerun_failures
294 sel
= selectors
.DefaultSelector()
295 total_tests
= len(tests
)
296 logger
.info("Total tests: %d" % total_tests
)
297 test_queue
= [(t
, 0) for t
in tests
]
301 scr
.addstr(0, 0, "Parallel test execution status", curses
.A_BOLD
)
302 for i
in range(0, num_servers
):
303 scr
.addstr(i
+ 1, 0, "VM %d:" % (i
+ 1), curses
.A_BOLD
)
304 status
= "starting VM" if vm
[i
]['proc'] else "not yet started"
305 scr
.addstr(i
+ 1, 10, status
)
306 scr
.addstr(num_servers
+ 1, 0, "Total:", curses
.A_BOLD
)
307 scr
.addstr(num_servers
+ 1, 20, "TOTAL={} STARTED=0 PASS=0 FAIL=0 SKIP=0".format(total_tests
))
312 events
= sel
.select(timeout
=1)
313 for key
, mask
in events
:
318 if vm_read_stdout(_vm
, test_queue
):
319 vm_next_step(_vm
, scr
, test_queue
)
322 if _vm
['proc'].poll() is not None:
323 if vm_terminated(_vm
, scr
, sel
, test_queue
):
326 running
, run_update
= check_vm_start(scr
, sel
, test_queue
)
327 if updated
or run_update
:
328 update_screen(scr
, total_tests
)
333 for i
in range(num_servers
):
334 if not vm
[i
]['proc']:
339 scr
.addstr("still running")
340 logger
.info("VM[%d] still running" % i
)
353 global first_run_failures
354 global total_started
, total_passed
, total_failed
, total_skipped
355 global rerun_failures
362 debug_level
= logging
.INFO
363 rerun_failures
= True
364 timestamp
= int(time
.time())
366 scriptsdir
= os
.path
.dirname(os
.path
.realpath(sys
.argv
[0]))
368 p
= argparse
.ArgumentParser(description
='run multiple testing VMs in parallel')
369 p
.add_argument('num_servers', metavar
='number of VMs', type=int, choices
=range(1, 100),
370 help="number of VMs to start")
371 p
.add_argument('-f', dest
='testmodules', metavar
='<test module>',
372 help='execute only tests from these test modules',
374 p
.add_argument('-1', dest
='no_retry', action
='store_const', const
=True, default
=False,
375 help="don't retry failed tests automatically")
376 p
.add_argument('--debug', dest
='debug', action
='store_const', const
=True, default
=False,
377 help="enable debug logging")
378 p
.add_argument('--codecov', dest
='codecov', action
='store_const', const
=True, default
=False,
379 help="enable code coverage collection")
380 p
.add_argument('--shuffle-tests', dest
='shuffle', action
='store_const', const
=True, default
=False,
381 help="shuffle test cases to randomize order")
382 p
.add_argument('--short', dest
='short', action
='store_const', const
=True,
384 help="only run short-duration test cases")
385 p
.add_argument('--long', dest
='long', action
='store_const', const
=True,
387 help="include long-duration test cases")
388 p
.add_argument('--valgrind', dest
='valgrind', action
='store_const',
389 const
=True, default
=False,
390 help="run tests under valgrind")
391 p
.add_argument('--telnet', dest
='telnet', metavar
='<baseport>', type=int,
392 help="enable telnet server inside VMs, specify the base port here")
393 p
.add_argument('--nocurses', dest
='nocurses', action
='store_const',
394 const
=True, default
=False, help="Don't use curses for output")
395 p
.add_argument('params', nargs
='*')
396 args
= p
.parse_args()
398 dir = os
.environ
.get('HWSIM_TEST_LOG_DIR', '/tmp/hwsim-test-logs')
402 if e
.errno
!= errno
.EEXIST
:
405 num_servers
= args
.num_servers
406 rerun_failures
= not args
.no_retry
408 debug_level
= logging
.DEBUG
411 extra_args
+= ['--valgrind']
413 extra_args
+= ['--long']
415 print("Code coverage - build separate binaries")
416 logdir
= os
.path
.join(dir, str(timestamp
))
418 subprocess
.check_call([os
.path
.join(scriptsdir
, 'build-codecov.sh'),
420 codecov_args
= ['--codecov_dir', logdir
]
426 first_run_failures
= []
431 cmd
= [os
.path
.join(os
.path
.dirname(scriptsdir
), 'run-tests.py'), '-L']
434 cmd
+= args
.testmodules
435 lst
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
)
436 for l
in lst
.stdout
.readlines():
437 name
= l
.decode().split(' ')[0]
440 sys
.exit("No test cases selected")
443 from random
import shuffle
445 elif num_servers
> 2 and len(tests
) > 100:
446 # Move test cases with long duration to the beginning as an
447 # optimization to avoid last part of the test execution running a long
448 # duration test case on a single VM while all other VMs have already
449 # completed their work.
455 tests
= [t
for t
in tests
if t
not in long_tests
]
457 logger
.setLevel(debug_level
)
458 if not args
.nocurses
:
459 log_handler
= logging
.FileHandler('parallel-vm.log')
461 log_handler
= logging
.StreamHandler(sys
.stdout
)
462 log_handler
.setLevel(debug_level
)
463 fmt
= "%(asctime)s %(levelname)s %(message)s"
464 log_formatter
= logging
.Formatter(fmt
)
465 log_handler
.setFormatter(log_formatter
)
466 logger
.addHandler(log_handler
)
469 for i
in range(0, num_servers
):
470 cmd
= [os
.path
.join(scriptsdir
, 'vm-run.sh'),
471 '--timestamp', str(timestamp
),
472 '--ext', 'srv.%d' % (i
+ 1),
473 '-i'] + codecov_args
+ extra_args
475 cmd
+= ['--telnet', str(args
.telnet
+ i
)]
478 vm
[i
]['starting'] = False
479 vm
[i
]['started'] = False
483 vm
[i
]['pending'] = ""
486 vm
[i
]['fail_seq'] = []
487 vm
[i
]['skip_reason'] = []
490 if not args
.nocurses
:
491 curses
.wrapper(show_progress
)
494 def leaveok(self
, n
):
498 def addstr(self
, *args
, **kw
):
500 def move(self
, x
, y
):
504 show_progress(FakeScreen())
506 with
open('{}/{}-parallel.log'.format(dir, timestamp
), 'w') as f
:
507 for i
in range(0, num_servers
):
508 f
.write('VM {}\n{}\n{}\n'.format(i
+ 1, vm
[i
]['out'], vm
[i
]['err']))
510 for i
in range(0, num_servers
):
511 for line
in vm
[i
]['out'].splitlines():
512 if line
.startswith("FAIL "):
515 print("Logs for failed test cases:")
516 f
.write("Logs for failed test cases:\n")
517 fname
= "%s/%d.srv.%d/%s.log" % (dir, timestamp
, i
+ 1,
520 f
.write("%s\n" % fname
)
522 failed
= get_failed(vm
)
524 if first_run_failures
:
525 print("To re-run same failure sequence(s):")
526 for i
in range(0, num_servers
):
527 if len(vm
[i
]['failed']) == 0:
529 print("./vm-run.sh", end
=' ')
531 print("--long", end
=' ')
532 skip
= len(vm
[i
]['fail_seq'])
533 skip
-= min(skip
, 30)
534 for t
in vm
[i
]['fail_seq']:
540 print("Failed test cases:")
541 for f
in first_run_failures
:
543 logger
.info("Failed: " + f
)
547 double_failed
.append(name
)
548 for test
in first_run_failures
:
549 double_failed
.remove(test
)
550 if not rerun_failures
:
552 elif failed
and not double_failed
:
553 print("All failed cases passed on retry")
554 logger
.info("All failed cases passed on retry")
556 print("Failed even on retry:")
557 for f
in double_failed
:
559 logger
.info("Failed on retry: " + f
)
561 res
= "TOTAL={} PASS={} FAIL={} SKIP={}".format(total_started
,
567 print("Logs: " + dir + '/' + str(timestamp
))
568 logger
.info("Logs: " + dir + '/' + str(timestamp
))
571 for i
in range(num_servers
):
572 if not vm
[i
]['started']:
574 skip_reason
+= vm
[i
]['skip_reason']
575 if len(vm
[i
]['pending']) > 0:
576 logger
.info("Unprocessed stdout from VM[%d]: '%s'" %
577 (i
, vm
[i
]['pending']))
578 log
= '{}/{}.srv.{}/console'.format(dir, timestamp
, i
+ 1)
579 with
open(log
, 'r') as f
:
580 if "Kernel panic" in f
.read():
581 print("Kernel panic in " + log
)
582 logger
.info("Kernel panic in " + log
)
584 missing
['OCV not supported'] = 'OCV'
585 missing
['sigma_dut not available'] = 'sigma_dut'
586 missing
['Skip test case with long duration due to --long not specified'] = 'long'
587 missing
['TEST_ALLOC_FAIL not supported' ] = 'TEST_FAIL'
588 missing
['TEST_ALLOC_FAIL not supported in the build'] = 'TEST_FAIL'
589 missing
['TEST_FAIL not supported' ] = 'TEST_FAIL'
590 missing
['veth not supported (kernel CONFIG_VETH)'] = 'KERNEL:CONFIG_VETH'
591 missing
['WPA-EAP-SUITE-B-192 not supported'] = 'CONFIG_SUITEB192'
592 missing
['WPA-EAP-SUITE-B not supported'] = 'CONFIG_SUITEB'
593 missing
['wmediumd not available'] = 'wmediumd'
594 missing
['DPP not supported'] = 'CONFIG_DPP'
595 missing
['DPP version 2 not supported'] = 'CONFIG_DPP2'
596 missing
['EAP method PWD not supported in the build'] = 'CONFIG_EAP_PWD'
597 missing
['EAP method TEAP not supported in the build'] = 'CONFIG_EAP_TEAP'
598 missing
['FILS not supported'] = 'CONFIG_FILS'
599 missing
['FILS-SK-PFS not supported'] = 'CONFIG_FILS_SK_PFS'
600 missing
['OWE not supported'] = 'CONFIG_OWE'
601 missing
['SAE not supported'] = 'CONFIG_SAE'
602 missing
['Not using OpenSSL'] = 'CONFIG_TLS=openssl'
603 missing
['wpa_supplicant TLS library is not OpenSSL: internal'] = 'CONFIG_TLS=openssl'
606 for reason
in sorted(set(skip_reason
)):
607 if reason
in missing
:
608 missing_items
.append(missing
[reason
])
609 elif reason
.startswith('OCSP-multi not supported with this TLS library'):
610 missing_items
.append('OCSP-MULTI')
612 other_reasons
.append(reason
)
614 print("Missing items (SKIP):", missing_items
)
616 print("Other skip reasons:", other_reasons
)
619 print("Code coverage - preparing report")
620 for i
in range(num_servers
):
621 subprocess
.check_call([os
.path
.join(scriptsdir
,
622 'process-codecov.sh'),
623 logdir
+ ".srv.%d" % (i
+ 1),
625 subprocess
.check_call([os
.path
.join(scriptsdir
, 'combine-codecov.sh'),
627 print("file://%s/index.html" % logdir
)
628 logger
.info("Code coverage report: file://%s/index.html" % logdir
)
630 if double_failed
or (failed
and not rerun_failures
):
631 logger
.info("Test run complete - failures found")
634 logger
.info("Test run complete - failures found on first run; passed on retry")
636 logger
.info("Test run complete - no failures")
639 if __name__
== "__main__":