]>
git.proxmox.com Git - mirror_zfs.git/blob - tests/test-runner/bin/test-runner.py.in
1 #!/usr/bin/env @PYTHON_SHEBANG@
4 # This file and its contents are supplied under the terms of the
5 # Common Development and Distribution License ("CDDL"), version 1.0.
6 # You may only use this file in accordance with the terms of version
9 # A full copy of the text of the CDDL should have accompanied this
10 # source. A copy of the CDDL is also available via the Internet at
11 # http://www.illumos.org/license/CDDL.
15 # Copyright (c) 2012, 2018 by Delphix. All rights reserved.
16 # Copyright (c) 2019 Datto Inc.
18 # This script must remain compatible with Python 3.6+.
27 from datetime
import datetime
28 from optparse
import OptionParser
29 from pwd
import getpwnam
30 from pwd
import getpwuid
31 from select
import select
32 from subprocess
import PIPE
33 from subprocess
import Popen
34 from subprocess
import check_output
35 from threading
import Timer
36 from time
import time
, CLOCK_MONOTONIC
37 from os
.path
import exists
39 BASEDIR
= '/var/tmp/test_results'
40 TESTDIR
= '/usr/share/zfs/'
41 KMEMLEAK_FILE
= '/sys/kernel/debug/kmemleak'
51 from time
import monotonic
as monotonic_time
53 class timespec(ctypes
.Structure
):
55 ('tv_sec', ctypes
.c_long
),
56 ('tv_nsec', ctypes
.c_long
)
59 librt
= ctypes
.CDLL('librt.so.1', use_errno
=True)
60 clock_gettime
= librt
.clock_gettime
61 clock_gettime
.argtypes
= [ctypes
.c_int
, ctypes
.POINTER(timespec
)]
65 if clock_gettime(CLOCK_MONOTONIC
, ctypes
.pointer(t
)) != 0:
66 errno_
= ctypes
.get_errno()
67 raise OSError(errno_
, os
.strerror(errno_
))
68 return t
.tv_sec
+ t
.tv_nsec
* 1e-9
73 runresults
= {'PASS': 0, 'FAIL': 0, 'SKIP': 0, 'KILLED': 0, 'RERAN': 0}
77 self
.returncode
= None
84 def done(self
, proc
, killed
, reran
):
86 Finalize the results of this Cmd.
89 m
, s
= divmod(monotonic_time() - self
.starttime
, 60)
90 self
.runtime
= '%02d:%02d' % (m
, s
)
91 self
.returncode
= proc
.returncode
93 Result
.runresults
['RERAN'] += 1
95 self
.result
= 'KILLED'
96 Result
.runresults
['KILLED'] += 1
97 elif len(self
.kmemleak
) > 0:
99 Result
.runresults
['FAIL'] += 1
100 elif self
.returncode
== 0:
102 Result
.runresults
['PASS'] += 1
103 elif self
.returncode
== 4:
105 Result
.runresults
['SKIP'] += 1
106 elif self
.returncode
!= 0:
108 Result
.runresults
['FAIL'] += 1
111 class Output(object):
113 This class is a slightly modified version of the 'Stream' class found
114 here: http://goo.gl/aSGfv
116 def __init__(self
, stream
, debug
=False):
123 return self
.stream
.fileno()
125 def read(self
, drain
=0):
127 Read from the file descriptor. If 'drain' set, read until EOF.
129 while self
._read
() is not None:
135 Read up to 4k of data from this output stream. Collect the output
136 up to the last newline, and append it to any leftover data from a
137 previous call. The lines are stored as a (timestamp, data) tuple
138 for easy sorting/merging later.
141 buf
= os
.read(fd
, 4096)
145 os
.write(sys
.stderr
.fileno(), buf
)
150 buf
= self
._buf
+ buf
151 tmp
, rest
= buf
.rsplit(b
'\n', 1)
154 rows
= tmp
.split(b
'\n')
155 self
.lines
+= [(now
, r
) for r
in rows
]
161 def __init__(self
, pathname
, identifier
=None, outputdir
=None,
162 timeout
=None, user
=None, tags
=None):
163 self
.pathname
= pathname
164 self
.identifier
= identifier
165 self
.outputdir
= outputdir
or 'BASEDIR'
167 The timeout for tests is measured in wall-clock time
169 self
.timeout
= timeout
170 self
.user
= user
or ''
173 self
.result
= Result()
175 if self
.timeout
is None:
185 ''' % (self
.pathname
, self
.identifier
, self
.outputdir
, self
.timeout
, self
.user
)
187 def kill_cmd(self
, proc
, options
, kmemleak
, keyboard_interrupt
=False):
189 Kill a running command due to timeout, or ^C from the keyboard. If
190 sudo is required, this user was verified previously.
193 do_sudo
= len(self
.user
) != 0
196 cmd
= [SUDO
, KILL
, signal
, str(proc
.pid
)]
207 If this is not a user-initiated kill and the test has not been
208 reran before we consider if the test needs to be reran:
209 If the test has spent some time hibernating and didn't run the whole
210 length of time before being timed out we will rerun the test.
212 if keyboard_interrupt
is False and self
.reran
is None:
213 runtime
= monotonic_time() - self
.result
.starttime
214 if int(self
.timeout
) > runtime
:
217 self
.run(options
, dryrun
=False, kmemleak
=kmemleak
)
220 def update_cmd_privs(self
, cmd
, user
):
222 If a user has been specified to run this Cmd and we're not already
223 running as that user, prepend the appropriate sudo command to run
226 me
= getpwuid(os
.getuid())
228 if not user
or user
is me
:
229 if os
.path
.isfile(cmd
+'.ksh') and os
.access(cmd
+'.ksh', os
.X_OK
):
231 if os
.path
.isfile(cmd
+'.sh') and os
.access(cmd
+'.sh', os
.X_OK
):
235 if not os
.path
.isfile(cmd
):
236 if os
.path
.isfile(cmd
+'.ksh') and os
.access(cmd
+'.ksh', os
.X_OK
):
238 if os
.path
.isfile(cmd
+'.sh') and os
.access(cmd
+'.sh', os
.X_OK
):
241 ret
= '%s -E -u %s %s' % (SUDO
, user
, cmd
)
242 return ret
.split(' ')
244 def collect_output(self
, proc
, debug
=False):
246 Read from stdout/stderr as data becomes available, until the
247 process is no longer running. Return the lines from the stdout and
248 stderr Output objects.
250 out
= Output(proc
.stdout
, debug
)
251 err
= Output(proc
.stderr
, debug
)
253 while proc
.returncode
is None:
255 res
= select([out
, err
], [], [], .1)
261 return out
.lines
, err
.lines
263 def run(self
, options
, dryrun
=None, kmemleak
=None):
265 This is the main function that runs each individual test.
266 Determine whether or not the command requires sudo, and modify it
267 if needed. Run the command, and update the result object.
270 dryrun
= options
.dryrun
275 kmemleak
= options
.kmemleak
277 privcmd
= self
.update_cmd_privs(self
.pathname
, self
.user
)
280 if not os
.path
.isdir(self
.outputdir
):
281 os
.makedirs(self
.outputdir
, mode
=0o777)
287 Log each test we run to /dev/kmsg (on Linux), so if there's a kernel
288 warning we'll be able to match it up to a particular test.
290 if options
.kmsg
is True and exists("/dev/kmsg"):
292 kp
= Popen([SUDO
, "sh", "-c",
293 f
"echo ZTS run {self.pathname} > /dev/kmsg"])
298 self
.result
.starttime
= monotonic_time()
301 cmd
= f
'{SUDO} sh -c "echo clear > {KMEMLEAK_FILE}"'
302 check_output(cmd
, shell
=True)
304 proc
= Popen(privcmd
, stdout
=PIPE
, stderr
=PIPE
)
305 # Allow a special timeout value of 0 to mean infinity
306 if int(self
.timeout
) == 0:
307 self
.timeout
= sys
.maxsize
/ (10 ** 9)
309 int(self
.timeout
), self
.kill_cmd
, [proc
, options
, kmemleak
]
315 out
, err
= self
.collect_output(proc
, options
.debug
)
316 self
.result
.stdout
= out
317 self
.result
.stderr
= err
320 cmd
= f
'{SUDO} sh -c "echo scan > {KMEMLEAK_FILE}"'
321 check_output(cmd
, shell
=True)
322 cmd
= f
'{SUDO} cat {KMEMLEAK_FILE}'
323 self
.result
.kmemleak
= check_output(cmd
, shell
=True)
324 except KeyboardInterrupt:
325 self
.kill_cmd(proc
, options
, kmemleak
, True)
326 fail('\nRun terminated at user request.')
330 if self
.reran
is not False:
331 self
.result
.done(proc
, self
.killed
, self
.reran
)
335 Initialize enough of the test result that we can log a skipped
339 Result
.runresults
['SKIP'] += 1
340 self
.result
.stdout
= self
.result
.stderr
= []
341 self
.result
.starttime
= monotonic_time()
342 m
, s
= divmod(monotonic_time() - self
.result
.starttime
, 60)
343 self
.result
.runtime
= '%02d:%02d' % (m
, s
)
344 self
.result
.result
= 'SKIP'
346 def log(self
, options
, suppress_console
=False):
348 This function is responsible for writing all output. This includes
349 the console output, the logfile of all results (with timestamped
350 merged stdout and stderr), and for each test, the unmodified
351 stdout/stderr/merged in its own file.
354 logname
= getpwuid(os
.getuid()).pw_name
356 if self
.reran
is True:
358 user
= ' (run as %s)' % (self
.user
if len(self
.user
) else logname
)
360 msga
= 'Test (%s): %s%s ' % (self
.identifier
, self
.pathname
, user
)
362 msga
= 'Test: %s%s ' % (self
.pathname
, user
)
363 msgb
= '[%s] [%s]%s\n' % (self
.result
.runtime
, self
.result
.result
, rer
)
364 pad
= ' ' * (80 - (len(msga
) + len(msgb
)))
365 result_line
= msga
+ pad
+ msgb
367 # The result line is always written to the log file. If -q was
368 # specified only failures are written to the console, otherwise
369 # the result line is written to the console. The console output
370 # may be suppressed by calling log() with suppress_console=True.
371 write_log(bytearray(result_line
, encoding
='utf-8'), LOG_FILE
)
372 if not suppress_console
:
373 if not options
.quiet
:
374 write_log(result_line
, LOG_OUT
)
375 elif options
.quiet
and self
.result
.result
!= 'PASS':
376 write_log(result_line
, LOG_OUT
)
378 lines
= sorted(self
.result
.stdout
+ self
.result
.stderr
,
381 # Write timestamped output (stdout and stderr) to the logfile
382 for dt
, line
in lines
:
383 timestamp
= bytearray(dt
.strftime("%H:%M:%S.%f ")[:11],
385 write_log(b
'%s %s\n' % (timestamp
, line
), LOG_FILE
)
387 # Write the separate stdout/stderr/merged files, if the data exists
388 if len(self
.result
.stdout
):
389 with
open(os
.path
.join(self
.outputdir
, 'stdout'), 'wb') as out
:
390 for _
, line
in self
.result
.stdout
:
391 os
.write(out
.fileno(), b
'%s\n' % line
)
392 if len(self
.result
.stderr
):
393 with
open(os
.path
.join(self
.outputdir
, 'stderr'), 'wb') as err
:
394 for _
, line
in self
.result
.stderr
:
395 os
.write(err
.fileno(), b
'%s\n' % line
)
396 if len(self
.result
.stdout
) and len(self
.result
.stderr
):
397 with
open(os
.path
.join(self
.outputdir
, 'merged'), 'wb') as merged
:
398 for _
, line
in lines
:
399 os
.write(merged
.fileno(), b
'%s\n' % line
)
400 if len(self
.result
.kmemleak
):
401 with
open(os
.path
.join(self
.outputdir
, 'kmemleak'), 'wb') as kmem
:
402 kmem
.write(self
.result
.kmemleak
)
406 props
= ['outputdir', 'timeout', 'user', 'pre', 'pre_user', 'post',
407 'post_user', 'failsafe', 'failsafe_user', 'tags']
409 def __init__(self
, pathname
,
410 pre
=None, pre_user
=None, post
=None, post_user
=None,
411 failsafe
=None, failsafe_user
=None, tags
=None, **kwargs
):
412 super(Test
, self
).__init
__(pathname
, **kwargs
)
414 self
.pre_user
= pre_user
or ''
415 self
.post
= post
or ''
416 self
.post_user
= post_user
or ''
417 self
.failsafe
= failsafe
or ''
418 self
.failsafe_user
= failsafe_user
or ''
419 self
.tags
= tags
or []
422 post_user
= pre_user
= failsafe_user
= ''
423 if len(self
.pre_user
):
424 pre_user
= ' (as %s)' % (self
.pre_user
)
425 if len(self
.post_user
):
426 post_user
= ' (as %s)' % (self
.post_user
)
427 if len(self
.failsafe_user
):
428 failsafe_user
= ' (as %s)' % (self
.failsafe_user
)
439 ''' % (self
.pathname
, self
.identifier
, self
.outputdir
, self
.timeout
, self
.user
,
440 self
.pre
, pre_user
, self
.post
, post_user
, self
.failsafe
,
441 failsafe_user
, self
.tags
)
445 Check the pre/post/failsafe scripts, user and Test. Omit the Test from
446 this run if there are any problems.
448 files
= [self
.pre
, self
.pathname
, self
.post
, self
.failsafe
]
449 users
= [self
.pre_user
, self
.user
, self
.post_user
, self
.failsafe_user
]
451 for f
in [f
for f
in files
if len(f
)]:
452 if not verify_file(f
):
453 write_log("Warning: Test '%s' not added to this run because"
454 " it failed verification.\n" % f
, LOG_ERR
)
457 for user
in [user
for user
in users
if len(user
)]:
458 if not verify_user(user
):
459 write_log("Not adding Test '%s' to this run.\n" %
460 self
.pathname
, LOG_ERR
)
465 def run(self
, options
, dryrun
=None, kmemleak
=None):
467 Create Cmd instances for the pre/post/failsafe scripts. If the pre
468 script doesn't pass, skip this Test. Run the post script regardless.
469 If the Test is killed, also run the failsafe script.
471 odir
= os
.path
.join(self
.outputdir
, os
.path
.basename(self
.pre
))
472 pretest
= Cmd(self
.pre
, identifier
=self
.identifier
, outputdir
=odir
,
473 timeout
=self
.timeout
, user
=self
.pre_user
)
474 test
= Cmd(self
.pathname
, identifier
=self
.identifier
,
475 outputdir
=self
.outputdir
, timeout
=self
.timeout
,
477 odir
= os
.path
.join(self
.outputdir
, os
.path
.basename(self
.failsafe
))
478 failsafe
= Cmd(self
.failsafe
, identifier
=self
.identifier
,
479 outputdir
=odir
, timeout
=self
.timeout
,
480 user
=self
.failsafe_user
)
481 odir
= os
.path
.join(self
.outputdir
, os
.path
.basename(self
.post
))
482 posttest
= Cmd(self
.post
, identifier
=self
.identifier
, outputdir
=odir
,
483 timeout
=self
.timeout
, user
=self
.post_user
)
486 if len(pretest
.pathname
):
487 pretest
.run(options
, kmemleak
=False)
488 cont
= pretest
.result
.result
== 'PASS'
492 test
.run(options
, kmemleak
=kmemleak
)
493 if test
.result
.result
== 'KILLED' and len(failsafe
.pathname
):
494 failsafe
.run(options
, kmemleak
=False)
495 failsafe
.log(options
, suppress_console
=True)
501 if len(posttest
.pathname
):
502 posttest
.run(options
, kmemleak
=False)
503 posttest
.log(options
)
506 class TestGroup(Test
):
507 props
= Test
.props
+ ['tests']
509 def __init__(self
, pathname
, tests
=None, **kwargs
):
510 super(TestGroup
, self
).__init
__(pathname
, **kwargs
)
511 self
.tests
= tests
or []
514 post_user
= pre_user
= failsafe_user
= ''
515 if len(self
.pre_user
):
516 pre_user
= ' (as %s)' % (self
.pre_user
)
517 if len(self
.post_user
):
518 post_user
= ' (as %s)' % (self
.post_user
)
519 if len(self
.failsafe_user
):
520 failsafe_user
= ' (as %s)' % (self
.failsafe_user
)
532 ''' % (self
.pathname
, self
.identifier
, self
.outputdir
, self
.tests
,
533 self
.timeout
, self
.user
, self
.pre
, pre_user
, self
.post
, post_user
,
534 self
.failsafe
, failsafe_user
, self
.tags
)
536 def filter(self
, keeplist
):
537 self
.tests
= [x
for x
in self
.tests
if x
in keeplist
]
541 Check the pre/post/failsafe scripts, user and tests in this TestGroup.
542 Omit the TestGroup entirely, or simply delete the relevant tests in the
543 group, if that's all that's required.
545 # If the pre/post/failsafe scripts are relative pathnames, convert to
546 # absolute, so they stand a chance of passing verification.
547 if len(self
.pre
) and not os
.path
.isabs(self
.pre
):
548 self
.pre
= os
.path
.join(self
.pathname
, self
.pre
)
549 if len(self
.post
) and not os
.path
.isabs(self
.post
):
550 self
.post
= os
.path
.join(self
.pathname
, self
.post
)
551 if len(self
.failsafe
) and not os
.path
.isabs(self
.failsafe
):
552 self
.post
= os
.path
.join(self
.pathname
, self
.post
)
554 auxfiles
= [self
.pre
, self
.post
, self
.failsafe
]
555 users
= [self
.pre_user
, self
.user
, self
.post_user
, self
.failsafe_user
]
557 for f
in [f
for f
in auxfiles
if len(f
)]:
558 if f
!= self
.failsafe
and self
.pathname
!= os
.path
.dirname(f
):
559 write_log("Warning: TestGroup '%s' not added to this run. "
560 "Auxiliary script '%s' exists in a different "
561 "directory.\n" % (self
.pathname
, f
), LOG_ERR
)
564 if not verify_file(f
):
565 write_log("Warning: TestGroup '%s' not added to this run. "
566 "Auxiliary script '%s' failed verification.\n" %
567 (self
.pathname
, f
), LOG_ERR
)
570 for user
in [user
for user
in users
if len(user
)]:
571 if not verify_user(user
):
572 write_log("Not adding TestGroup '%s' to this run.\n" %
573 self
.pathname
, LOG_ERR
)
576 # If one of the tests is invalid, delete it, log it, and drive on.
577 for test
in self
.tests
:
578 if not verify_file(os
.path
.join(self
.pathname
, test
)):
579 del self
.tests
[self
.tests
.index(test
)]
580 write_log("Warning: Test '%s' removed from TestGroup '%s' "
581 "because it failed verification.\n" %
582 (test
, self
.pathname
), LOG_ERR
)
584 return len(self
.tests
) != 0
586 def run(self
, options
, dryrun
=None, kmemleak
=None):
588 Create Cmd instances for the pre/post/failsafe scripts. If the pre
589 script doesn't pass, skip all the tests in this TestGroup. Run the
590 post script regardless. Run the failsafe script when a test is killed.
592 # tags assigned to this test group also include the test names
593 if options
.tags
and not set(self
.tags
).intersection(set(options
.tags
)):
596 odir
= os
.path
.join(self
.outputdir
, os
.path
.basename(self
.pre
))
597 pretest
= Cmd(self
.pre
, outputdir
=odir
, timeout
=self
.timeout
,
598 user
=self
.pre_user
, identifier
=self
.identifier
)
599 odir
= os
.path
.join(self
.outputdir
, os
.path
.basename(self
.post
))
600 posttest
= Cmd(self
.post
, outputdir
=odir
, timeout
=self
.timeout
,
601 user
=self
.post_user
, identifier
=self
.identifier
)
604 if len(pretest
.pathname
):
605 pretest
.run(options
, dryrun
=dryrun
, kmemleak
=False)
606 cont
= pretest
.result
.result
== 'PASS'
609 for fname
in self
.tests
:
610 odir
= os
.path
.join(self
.outputdir
, fname
)
611 test
= Cmd(os
.path
.join(self
.pathname
, fname
), outputdir
=odir
,
612 timeout
=self
.timeout
, user
=self
.user
,
613 identifier
=self
.identifier
)
614 odir
= os
.path
.join(odir
, os
.path
.basename(self
.failsafe
))
615 failsafe
= Cmd(self
.failsafe
, outputdir
=odir
, timeout
=self
.timeout
,
616 user
=self
.failsafe_user
, identifier
=self
.identifier
)
618 test
.run(options
, dryrun
=dryrun
, kmemleak
=kmemleak
)
619 if test
.result
.result
== 'KILLED' and len(failsafe
.pathname
):
620 failsafe
.run(options
, dryrun
=dryrun
, kmemleak
=False)
621 failsafe
.log(options
, suppress_console
=True)
627 if len(posttest
.pathname
):
628 posttest
.run(options
, dryrun
=dryrun
, kmemleak
=False)
629 posttest
.log(options
)
632 class TestRun(object):
633 props
= ['quiet', 'outputdir', 'debug']
635 def __init__(self
, options
):
638 self
.starttime
= time()
639 self
.timestamp
= datetime
.now().strftime('%Y%m%dT%H%M%S')
640 self
.outputdir
= os
.path
.join(options
.outputdir
, self
.timestamp
)
641 self
.setup_logging(options
)
643 ('outputdir', BASEDIR
),
652 ('failsafe_user', ''),
658 s
= 'TestRun:\n outputdir: %s\n' % self
.outputdir
660 for key
in sorted(self
.tests
.keys()):
661 s
+= '%s%s' % (self
.tests
[key
].__str
__(), '\n')
663 for key
in sorted(self
.testgroups
.keys()):
664 s
+= '%s%s' % (self
.testgroups
[key
].__str
__(), '\n')
667 def addtest(self
, pathname
, options
):
669 Create a new Test, and apply any properties that were passed in
670 from the command line. If it passes verification, add it to the
673 test
= Test(pathname
)
674 for prop
in Test
.props
:
675 setattr(test
, prop
, getattr(options
, prop
))
678 self
.tests
[pathname
] = test
680 def addtestgroup(self
, dirname
, filenames
, options
):
682 Create a new TestGroup, and apply any properties that were passed
683 in from the command line. If it passes verification, add it to the
686 if dirname
not in self
.testgroups
:
687 testgroup
= TestGroup(dirname
)
688 for prop
in Test
.props
:
689 setattr(testgroup
, prop
, getattr(options
, prop
))
691 # Prevent pre/post/failsafe scripts from running as regular tests
692 for f
in [testgroup
.pre
, testgroup
.post
, testgroup
.failsafe
]:
694 del filenames
[filenames
.index(f
)]
696 self
.testgroups
[dirname
] = testgroup
697 self
.testgroups
[dirname
].tests
= sorted(filenames
)
701 def filter(self
, keeplist
):
702 for group
in list(self
.testgroups
.keys()):
703 if group
not in keeplist
:
704 del self
.testgroups
[group
]
707 g
= self
.testgroups
[group
]
709 if g
.pre
and os
.path
.basename(g
.pre
) in keeplist
[group
]:
712 g
.filter(keeplist
[group
])
714 for test
in list(self
.tests
.keys()):
715 directory
, base
= os
.path
.split(test
)
716 if directory
not in keeplist
or base
not in keeplist
[directory
]:
719 def read(self
, options
):
721 Read in the specified runfiles, and apply the TestRun properties
722 listed in the 'DEFAULT' section to our TestRun. Then read each
723 section, and apply the appropriate properties to the Test or
724 TestGroup. Properties from individual sections override those set
725 in the 'DEFAULT' section. If the Test or TestGroup passes
726 verification, add it to the TestRun.
728 config
= configparser
.RawConfigParser()
729 parsed
= config
.read(options
.runfiles
)
730 failed
= options
.runfiles
- set(parsed
)
732 files
= ' '.join(sorted(failed
))
733 fail("Couldn't read config files: %s" % files
)
735 for opt
in TestRun
.props
:
736 if config
.has_option('DEFAULT', opt
):
737 setattr(self
, opt
, config
.get('DEFAULT', opt
))
738 self
.outputdir
= os
.path
.join(self
.outputdir
, self
.timestamp
)
740 testdir
= options
.testdir
742 for section
in config
.sections():
743 if 'tests' in config
.options(section
):
744 parts
= section
.split(':', 1)
745 sectiondir
= parts
[0]
746 identifier
= parts
[1] if len(parts
) == 2 else None
747 if os
.path
.isdir(sectiondir
):
748 pathname
= sectiondir
749 elif os
.path
.isdir(os
.path
.join(testdir
, sectiondir
)):
750 pathname
= os
.path
.join(testdir
, sectiondir
)
752 pathname
= sectiondir
754 testgroup
= TestGroup(os
.path
.abspath(pathname
),
755 identifier
=identifier
)
756 for prop
in TestGroup
.props
:
757 for sect
in ['DEFAULT', section
]:
758 if config
.has_option(sect
, prop
):
760 setattr(testgroup
, prop
,
761 eval(config
.get(sect
, prop
)))
762 elif prop
== 'failsafe':
763 failsafe
= config
.get(sect
, prop
)
764 setattr(testgroup
, prop
,
765 os
.path
.join(testdir
, failsafe
))
767 setattr(testgroup
, prop
,
768 config
.get(sect
, prop
))
770 # Repopulate tests using eval to convert the string to a list
771 testgroup
.tests
= eval(config
.get(section
, 'tests'))
773 if testgroup
.verify():
774 self
.testgroups
[section
] = testgroup
777 for prop
in Test
.props
:
778 for sect
in ['DEFAULT', section
]:
779 if config
.has_option(sect
, prop
):
780 if prop
== 'failsafe':
781 failsafe
= config
.get(sect
, prop
)
783 os
.path
.join(testdir
, failsafe
))
785 setattr(test
, prop
, config
.get(sect
, prop
))
788 self
.tests
[section
] = test
790 def write(self
, options
):
792 Create a configuration file for editing and later use. The
793 'DEFAULT' section of the config file is created from the
794 properties that were specified on the command line. Tests are
795 simply added as sections that inherit everything from the
796 'DEFAULT' section. TestGroups are the same, except they get an
797 option including all the tests to run in that directory.
800 defaults
= dict([(prop
, getattr(options
, prop
)) for prop
, _
in
802 config
= configparser
.RawConfigParser(defaults
)
804 for test
in sorted(self
.tests
.keys()):
805 config
.add_section(test
)
806 for prop
in Test
.props
:
807 if prop
not in self
.props
:
808 config
.set(test
, prop
,
809 getattr(self
.tests
[test
], prop
))
811 for testgroup
in sorted(self
.testgroups
.keys()):
812 config
.add_section(testgroup
)
813 config
.set(testgroup
, 'tests', self
.testgroups
[testgroup
].tests
)
814 for prop
in TestGroup
.props
:
815 if prop
not in self
.props
:
816 config
.set(testgroup
, prop
,
817 getattr(self
.testgroups
[testgroup
], prop
))
820 with
open(options
.template
, 'w') as f
:
821 return config
.write(f
)
823 fail('Could not open \'%s\' for writing.' % options
.template
)
825 def complete_outputdirs(self
):
827 Collect all the pathnames for Tests, and TestGroups. Work
828 backwards one pathname component at a time, to create a unique
829 directory name in which to deposit test output. Tests will be able
830 to write output files directly in the newly modified outputdir.
831 TestGroups will be able to create one subdirectory per test in the
832 outputdir, and are guaranteed uniqueness because a group can only
833 contain files in one directory. Pre and post tests will create a
834 directory rooted at the outputdir of the Test or TestGroup in
835 question for their output. Failsafe scripts will create a directory
836 rooted at the outputdir of each Test for their output.
840 tmp_dict
= dict(list(self
.tests
.items()) +
841 list(self
.testgroups
.items()))
842 total
= len(tmp_dict
)
843 base
= self
.outputdir
848 for testfile
in list(tmp_dict
.keys()):
849 uniq
= '/'.join(testfile
.split('/')[components
:]).lstrip('/')
850 if uniq
not in paths
:
852 tmp_dict
[testfile
].outputdir
= os
.path
.join(base
, uniq
)
855 done
= total
== len(paths
)
857 def setup_logging(self
, options
):
859 This function creates the output directory and gets a file object
860 for the logfile. This function must be called before write_log()
863 if options
.dryrun
is True:
867 if not options
.template
:
870 os
.makedirs(self
.outputdir
, mode
=0o777)
872 filename
= os
.path
.join(self
.outputdir
, 'log')
873 LOG_FILE_OBJ
= open(filename
, buffering
=0, mode
='wb')
877 def run(self
, options
):
879 Walk through all the Tests and TestGroups, calling run().
882 os
.chdir(self
.outputdir
)
884 fail('Could not change to directory %s' % self
.outputdir
)
885 # make a symlink to the output for the currently running test
886 logsymlink
= os
.path
.join(self
.outputdir
, '../current')
887 if os
.path
.islink(logsymlink
):
888 os
.unlink(logsymlink
)
889 if not os
.path
.exists(logsymlink
):
890 os
.symlink(self
.outputdir
, logsymlink
)
892 write_log('Could not make a symlink to directory %s\n' %
893 self
.outputdir
, LOG_ERR
)
896 cmd
= f
'{SUDO} -c "echo scan=0 > {KMEMLEAK_FILE}"'
897 check_output(cmd
, shell
=True)
900 while iteration
< options
.iterations
:
901 for test
in sorted(self
.tests
.keys()):
902 self
.tests
[test
].run(options
)
903 for testgroup
in sorted(self
.testgroups
.keys()):
904 self
.testgroups
[testgroup
].run(options
)
908 if Result
.total
== 0:
911 print('\nResults Summary')
912 for key
in list(Result
.runresults
.keys()):
913 if Result
.runresults
[key
] != 0:
914 print('%s\t% 4d' % (key
, Result
.runresults
[key
]))
916 m
, s
= divmod(time() - self
.starttime
, 60)
918 print('\nRunning Time:\t%02d:%02d:%02d' % (h
, m
, s
))
919 print('Percent passed:\t%.1f%%' % ((float(Result
.runresults
['PASS']) /
920 float(Result
.total
)) * 100))
921 print('Log directory:\t%s' % self
.outputdir
)
923 if Result
.runresults
['FAIL'] > 0:
926 if Result
.runresults
['KILLED'] > 0:
929 if Result
.runresults
['RERAN'] > 0:
935 def write_log(msg
, target
):
937 Write the provided message to standard out, standard error or
938 the logfile. If specifying LOG_FILE, then `msg` must be a bytes
939 like object. This way we can still handle output from tests that
940 may be in unexpected encodings.
942 if target
== LOG_OUT
:
943 os
.write(sys
.stdout
.fileno(), bytearray(msg
, encoding
='utf-8'))
944 elif target
== LOG_ERR
:
945 os
.write(sys
.stderr
.fileno(), bytearray(msg
, encoding
='utf-8'))
946 elif target
== LOG_FILE
:
947 os
.write(LOG_FILE_OBJ
.fileno(), msg
)
949 fail('log_msg called with unknown target "%s"' % target
)
952 def verify_file(pathname
):
954 Verify that the supplied pathname is an executable regular file.
956 if os
.path
.isdir(pathname
) or os
.path
.islink(pathname
):
959 for ext
in '', '.ksh', '.sh':
960 script_path
= pathname
+ ext
961 if os
.path
.isfile(script_path
) and os
.access(script_path
, os
.X_OK
):
967 def verify_user(user
):
969 Verify that the specified user exists on this system, and can execute
970 sudo without being prompted for a password.
972 testcmd
= [SUDO
, '-n', '-u', user
, TRUE
]
974 if user
in Cmd
.verified_users
:
980 write_log("Warning: user '%s' does not exist.\n" % user
,
986 if p
.returncode
!= 0:
987 write_log("Warning: user '%s' cannot use passwordless sudo.\n" % user
,
991 Cmd
.verified_users
.append(user
)
996 def find_tests(testrun
, options
):
998 For the given list of pathnames, add files as Tests. For directories,
999 if do_groups is True, add the directory as a TestGroup. If False,
1000 recursively search for executable files.
1003 for p
in sorted(options
.pathnames
):
1004 if os
.path
.isdir(p
):
1005 for dirname
, _
, filenames
in os
.walk(p
):
1006 if options
.do_groups
:
1007 testrun
.addtestgroup(dirname
, filenames
, options
)
1009 for f
in sorted(filenames
):
1010 testrun
.addtest(os
.path
.join(dirname
, f
), options
)
1012 testrun
.addtest(p
, options
)
1015 def filter_tests(testrun
, options
):
1017 fh
= open(options
.logfile
, "r")
1018 except Exception as e
:
1023 line
= fh
.readline()
1026 m
= re
.match(r
'Test: .*(tests/.*)/(\S+).*\[FAIL\]', line
)
1029 group
, test
= m
.group(1, 2)
1031 failed
[group
].append(test
)
1033 failed
[group
] = [test
]
1036 testrun
.filter(failed
)
1039 def fail(retstr
, ret
=1):
1040 print('%s: %s' % (sys
.argv
[0], retstr
))
1044 def kmemleak_cb(option
, opt_str
, value
, parser
):
1045 if not os
.path
.exists(KMEMLEAK_FILE
):
1046 fail(f
"File '{KMEMLEAK_FILE}' doesn't exist. " +
1047 "Enable CONFIG_DEBUG_KMEMLEAK in kernel configuration.")
1049 setattr(parser
.values
, option
.dest
, True)
1052 def options_cb(option
, opt_str
, value
, parser
):
1053 path_options
= ['outputdir', 'template', 'testdir', 'logfile']
1055 if opt_str
in parser
.rargs
:
1056 fail('%s may only be specified once.' % opt_str
)
1058 if option
.dest
== 'runfiles':
1059 parser
.values
.cmd
= 'rdconfig'
1060 value
= set(os
.path
.abspath(p
) for p
in value
.split(','))
1061 if option
.dest
== 'tags':
1062 value
= [x
.strip() for x
in value
.split(',')]
1064 if option
.dest
in path_options
:
1065 setattr(parser
.values
, option
.dest
, os
.path
.abspath(value
))
1067 setattr(parser
.values
, option
.dest
, value
)
1071 parser
= OptionParser()
1072 parser
.add_option('-c', action
='callback', callback
=options_cb
,
1073 type='string', dest
='runfiles', metavar
='runfiles',
1074 help='Specify tests to run via config files.')
1075 parser
.add_option('-d', action
='store_true', default
=False, dest
='dryrun',
1076 help='Dry run. Print tests, but take no other action.')
1077 parser
.add_option('-D', action
='store_true', default
=False, dest
='debug',
1078 help='Write all test output to stdout as it arrives.')
1079 parser
.add_option('-l', action
='callback', callback
=options_cb
,
1080 default
=None, dest
='logfile', metavar
='logfile',
1082 help='Read logfile and re-run tests which failed.')
1083 parser
.add_option('-g', action
='store_true', default
=False,
1084 dest
='do_groups', help='Make directories TestGroups.')
1085 parser
.add_option('-o', action
='callback', callback
=options_cb
,
1086 default
=BASEDIR
, dest
='outputdir', type='string',
1087 metavar
='outputdir', help='Specify an output directory.')
1088 parser
.add_option('-i', action
='callback', callback
=options_cb
,
1089 default
=TESTDIR
, dest
='testdir', type='string',
1090 metavar
='testdir', help='Specify a test directory.')
1091 parser
.add_option('-K', action
='store_true', default
=False, dest
='kmsg',
1092 help='Log tests names to /dev/kmsg')
1093 parser
.add_option('-m', action
='callback', callback
=kmemleak_cb
,
1094 default
=False, dest
='kmemleak',
1095 help='Enable kmemleak reporting (Linux only)')
1096 parser
.add_option('-p', action
='callback', callback
=options_cb
,
1097 default
='', dest
='pre', metavar
='script',
1098 type='string', help='Specify a pre script.')
1099 parser
.add_option('-P', action
='callback', callback
=options_cb
,
1100 default
='', dest
='post', metavar
='script',
1101 type='string', help='Specify a post script.')
1102 parser
.add_option('-q', action
='store_true', default
=False, dest
='quiet',
1103 help='Silence on the console during a test run.')
1104 parser
.add_option('-s', action
='callback', callback
=options_cb
,
1105 default
='', dest
='failsafe', metavar
='script',
1106 type='string', help='Specify a failsafe script.')
1107 parser
.add_option('-S', action
='callback', callback
=options_cb
,
1108 default
='', dest
='failsafe_user',
1109 metavar
='failsafe_user', type='string',
1110 help='Specify a user to execute the failsafe script.')
1111 parser
.add_option('-t', action
='callback', callback
=options_cb
, default
=60,
1112 dest
='timeout', metavar
='seconds', type='int',
1113 help='Timeout (in seconds) for an individual test.')
1114 parser
.add_option('-u', action
='callback', callback
=options_cb
,
1115 default
='', dest
='user', metavar
='user', type='string',
1116 help='Specify a different user name to run as.')
1117 parser
.add_option('-w', action
='callback', callback
=options_cb
,
1118 default
=None, dest
='template', metavar
='template',
1119 type='string', help='Create a new config file.')
1120 parser
.add_option('-x', action
='callback', callback
=options_cb
, default
='',
1121 dest
='pre_user', metavar
='pre_user', type='string',
1122 help='Specify a user to execute the pre script.')
1123 parser
.add_option('-X', action
='callback', callback
=options_cb
, default
='',
1124 dest
='post_user', metavar
='post_user', type='string',
1125 help='Specify a user to execute the post script.')
1126 parser
.add_option('-T', action
='callback', callback
=options_cb
, default
='',
1127 dest
='tags', metavar
='tags', type='string',
1128 help='Specify tags to execute specific test groups.')
1129 parser
.add_option('-I', action
='callback', callback
=options_cb
, default
=1,
1130 dest
='iterations', metavar
='iterations', type='int',
1131 help='Number of times to run the test run.')
1132 (options
, pathnames
) = parser
.parse_args()
1134 if options
.runfiles
and len(pathnames
):
1135 fail('Extraneous arguments.')
1137 options
.pathnames
= [os
.path
.abspath(path
) for path
in pathnames
]
1143 options
= parse_args()
1145 testrun
= TestRun(options
)
1147 if options
.runfiles
:
1148 testrun
.read(options
)
1150 find_tests(testrun
, options
)
1153 filter_tests(testrun
, options
)
1155 if options
.template
:
1156 testrun
.write(options
)
1159 testrun
.complete_outputdirs()
1160 testrun
.run(options
)
1161 exit(testrun
.summary())
1164 if __name__
== '__main__':