]>
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 2.6+ and Python 3.4+.
21 # some python 2.7 system don't have a configparser shim
25 import ConfigParser
as configparser
32 from datetime
import datetime
33 from optparse
import OptionParser
34 from pwd
import getpwnam
35 from pwd
import getpwuid
36 from select
import select
37 from subprocess
import PIPE
38 from subprocess
import Popen
39 from subprocess
import check_output
40 from threading
import Timer
43 BASEDIR
= '/var/tmp/test_results'
44 TESTDIR
= '/usr/share/zfs/'
45 KMEMLEAK_FILE
= '/sys/kernel/debug/kmemleak'
54 # some python 2.7 system don't have a concept of monotonic time
55 CLOCK_MONOTONIC_RAW
= 4 # see <linux/time.h>
58 class timespec(ctypes
.Structure
):
60 ('tv_sec', ctypes
.c_long
),
61 ('tv_nsec', ctypes
.c_long
)
65 librt
= ctypes
.CDLL('librt.so.1', use_errno
=True)
66 clock_gettime
= librt
.clock_gettime
67 clock_gettime
.argtypes
= [ctypes
.c_int
, ctypes
.POINTER(timespec
)]
72 if clock_gettime(CLOCK_MONOTONIC_RAW
, ctypes
.pointer(t
)) != 0:
73 errno_
= ctypes
.get_errno()
74 raise OSError(errno_
, os
.strerror(errno_
))
75 return t
.tv_sec
+ t
.tv_nsec
* 1e-9
80 runresults
= {'PASS': 0, 'FAIL': 0, 'SKIP': 0, 'KILLED': 0, 'RERAN': 0}
84 self
.returncode
= None
91 def done(self
, proc
, killed
, reran
):
93 Finalize the results of this Cmd.
96 m
, s
= divmod(monotonic_time() - self
.starttime
, 60)
97 self
.runtime
= '%02d:%02d' % (m
, s
)
98 self
.returncode
= proc
.returncode
100 Result
.runresults
['RERAN'] += 1
102 self
.result
= 'KILLED'
103 Result
.runresults
['KILLED'] += 1
104 elif len(self
.kmemleak
) > 0:
106 Result
.runresults
['FAIL'] += 1
107 elif self
.returncode
== 0:
109 Result
.runresults
['PASS'] += 1
110 elif self
.returncode
== 4:
112 Result
.runresults
['SKIP'] += 1
113 elif self
.returncode
!= 0:
115 Result
.runresults
['FAIL'] += 1
118 class Output(object):
120 This class is a slightly modified version of the 'Stream' class found
121 here: http://goo.gl/aSGfv
123 def __init__(self
, stream
):
129 return self
.stream
.fileno()
131 def read(self
, drain
=0):
133 Read from the file descriptor. If 'drain' set, read until EOF.
135 while self
._read
() is not None:
141 Read up to 4k of data from this output stream. Collect the output
142 up to the last newline, and append it to any leftover data from a
143 previous call. The lines are stored as a (timestamp, data) tuple
144 for easy sorting/merging later.
147 buf
= os
.read(fd
, 4096)
154 buf
= self
._buf
+ buf
155 tmp
, rest
= buf
.rsplit(b
'\n', 1)
158 rows
= tmp
.split(b
'\n')
159 self
.lines
+= [(now
, r
) for r
in rows
]
165 def __init__(self
, pathname
, identifier
=None, outputdir
=None,
166 timeout
=None, user
=None, tags
=None):
167 self
.pathname
= pathname
168 self
.identifier
= identifier
169 self
.outputdir
= outputdir
or 'BASEDIR'
171 The timeout for tests is measured in wall-clock time
173 self
.timeout
= timeout
174 self
.user
= user
or ''
177 self
.result
= Result()
179 if self
.timeout
is None:
189 ''' % (self
.pathname
, self
.identifier
, self
.outputdir
, self
.timeout
, self
.user
)
191 def kill_cmd(self
, proc
, keyboard_interrupt
=False):
193 Kill a running command due to timeout, or ^C from the keyboard. If
194 sudo is required, this user was verified previously.
197 do_sudo
= len(self
.user
) != 0
200 cmd
= [SUDO
, KILL
, signal
, str(proc
.pid
)]
211 If this is not a user-initiated kill and the test has not been
212 reran before we consider if the test needs to be reran:
213 If the test has spent some time hibernating and didn't run the whole
214 length of time before being timed out we will rerun the test.
216 if keyboard_interrupt
is False and self
.reran
is None:
217 runtime
= monotonic_time() - self
.result
.starttime
218 if int(self
.timeout
) > runtime
:
224 def update_cmd_privs(self
, cmd
, user
):
226 If a user has been specified to run this Cmd and we're not already
227 running as that user, prepend the appropriate sudo command to run
230 me
= getpwuid(os
.getuid())
232 if not user
or user
is me
:
233 if os
.path
.isfile(cmd
+'.ksh') and os
.access(cmd
+'.ksh', os
.X_OK
):
235 if os
.path
.isfile(cmd
+'.sh') and os
.access(cmd
+'.sh', os
.X_OK
):
239 if not os
.path
.isfile(cmd
):
240 if os
.path
.isfile(cmd
+'.ksh') and os
.access(cmd
+'.ksh', os
.X_OK
):
242 if os
.path
.isfile(cmd
+'.sh') and os
.access(cmd
+'.sh', os
.X_OK
):
245 ret
= '%s -E -u %s %s' % (SUDO
, user
, cmd
)
246 return ret
.split(' ')
248 def collect_output(self
, proc
):
250 Read from stdout/stderr as data becomes available, until the
251 process is no longer running. Return the lines from the stdout and
252 stderr Output objects.
254 out
= Output(proc
.stdout
)
255 err
= Output(proc
.stderr
)
257 while proc
.returncode
is None:
259 res
= select([out
, err
], [], [], .1)
265 return out
.lines
, err
.lines
267 def run(self
, dryrun
, kmemleak
):
269 This is the main function that runs each individual test.
270 Determine whether or not the command requires sudo, and modify it
271 if needed. Run the command, and update the result object.
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)
286 self
.result
.starttime
= monotonic_time()
289 cmd
= f
'echo clear | {SUDO} tee {KMEMLEAK_FILE}'
290 check_output(cmd
, shell
=True)
292 proc
= Popen(privcmd
, stdout
=PIPE
, stderr
=PIPE
)
293 # Allow a special timeout value of 0 to mean infinity
294 if int(self
.timeout
) == 0:
295 self
.timeout
= sys
.maxsize
296 t
= Timer(int(self
.timeout
), self
.kill_cmd
, [proc
])
300 self
.result
.stdout
, self
.result
.stderr
= self
.collect_output(proc
)
303 cmd
= f
'echo scan | {SUDO} tee {KMEMLEAK_FILE}'
304 check_output(cmd
, shell
=True)
305 cmd
= f
'{SUDO} cat {KMEMLEAK_FILE}'
306 self
.result
.kmemleak
= check_output(cmd
, shell
=True)
307 except KeyboardInterrupt:
308 self
.kill_cmd(proc
, True)
309 fail('\nRun terminated at user request.')
313 if self
.reran
is not False:
314 self
.result
.done(proc
, self
.killed
, self
.reran
)
318 Initialize enough of the test result that we can log a skipped
322 Result
.runresults
['SKIP'] += 1
323 self
.result
.stdout
= self
.result
.stderr
= []
324 self
.result
.starttime
= monotonic_time()
325 m
, s
= divmod(monotonic_time() - self
.result
.starttime
, 60)
326 self
.result
.runtime
= '%02d:%02d' % (m
, s
)
327 self
.result
.result
= 'SKIP'
329 def log(self
, options
, suppress_console
=False):
331 This function is responsible for writing all output. This includes
332 the console output, the logfile of all results (with timestamped
333 merged stdout and stderr), and for each test, the unmodified
334 stdout/stderr/merged in its own file.
337 logname
= getpwuid(os
.getuid()).pw_name
339 if self
.reran
is True:
341 user
= ' (run as %s)' % (self
.user
if len(self
.user
) else logname
)
343 msga
= 'Test (%s): %s%s ' % (self
.identifier
, self
.pathname
, user
)
345 msga
= 'Test: %s%s ' % (self
.pathname
, user
)
346 msgb
= '[%s] [%s]%s\n' % (self
.result
.runtime
, self
.result
.result
, rer
)
347 pad
= ' ' * (80 - (len(msga
) + len(msgb
)))
348 result_line
= msga
+ pad
+ msgb
350 # The result line is always written to the log file. If -q was
351 # specified only failures are written to the console, otherwise
352 # the result line is written to the console. The console output
353 # may be suppressed by calling log() with suppress_console=True.
354 write_log(bytearray(result_line
, encoding
='utf-8'), LOG_FILE
)
355 if not suppress_console
:
356 if not options
.quiet
:
357 write_log(result_line
, LOG_OUT
)
358 elif options
.quiet
and self
.result
.result
!= 'PASS':
359 write_log(result_line
, LOG_OUT
)
361 lines
= sorted(self
.result
.stdout
+ self
.result
.stderr
,
364 # Write timestamped output (stdout and stderr) to the logfile
365 for dt
, line
in lines
:
366 timestamp
= bytearray(dt
.strftime("%H:%M:%S.%f ")[:11],
368 write_log(b
'%s %s\n' % (timestamp
, line
), LOG_FILE
)
370 # Write the separate stdout/stderr/merged files, if the data exists
371 if len(self
.result
.stdout
):
372 with
open(os
.path
.join(self
.outputdir
, 'stdout'), 'wb') as out
:
373 for _
, line
in self
.result
.stdout
:
374 os
.write(out
.fileno(), b
'%s\n' % line
)
375 if len(self
.result
.stderr
):
376 with
open(os
.path
.join(self
.outputdir
, 'stderr'), 'wb') as err
:
377 for _
, line
in self
.result
.stderr
:
378 os
.write(err
.fileno(), b
'%s\n' % line
)
379 if len(self
.result
.stdout
) and len(self
.result
.stderr
):
380 with
open(os
.path
.join(self
.outputdir
, 'merged'), 'wb') as merged
:
381 for _
, line
in lines
:
382 os
.write(merged
.fileno(), b
'%s\n' % line
)
383 if len(self
.result
.kmemleak
):
384 with
open(os
.path
.join(self
.outputdir
, 'kmemleak'), 'wb') as kmem
:
385 kmem
.write(self
.result
.kmemleak
)
389 props
= ['outputdir', 'timeout', 'user', 'pre', 'pre_user', 'post',
390 'post_user', 'failsafe', 'failsafe_user', 'tags']
392 def __init__(self
, pathname
,
393 pre
=None, pre_user
=None, post
=None, post_user
=None,
394 failsafe
=None, failsafe_user
=None, tags
=None, **kwargs
):
395 super(Test
, self
).__init
__(pathname
, **kwargs
)
397 self
.pre_user
= pre_user
or ''
398 self
.post
= post
or ''
399 self
.post_user
= post_user
or ''
400 self
.failsafe
= failsafe
or ''
401 self
.failsafe_user
= failsafe_user
or ''
402 self
.tags
= tags
or []
405 post_user
= pre_user
= failsafe_user
= ''
406 if len(self
.pre_user
):
407 pre_user
= ' (as %s)' % (self
.pre_user
)
408 if len(self
.post_user
):
409 post_user
= ' (as %s)' % (self
.post_user
)
410 if len(self
.failsafe_user
):
411 failsafe_user
= ' (as %s)' % (self
.failsafe_user
)
422 ''' % (self
.pathname
, self
.identifier
, self
.outputdir
, self
.timeout
, self
.user
,
423 self
.pre
, pre_user
, self
.post
, post_user
, self
.failsafe
,
424 failsafe_user
, self
.tags
)
428 Check the pre/post/failsafe scripts, user and Test. Omit the Test from
429 this run if there are any problems.
431 files
= [self
.pre
, self
.pathname
, self
.post
, self
.failsafe
]
432 users
= [self
.pre_user
, self
.user
, self
.post_user
, self
.failsafe_user
]
434 for f
in [f
for f
in files
if len(f
)]:
435 if not verify_file(f
):
436 write_log("Warning: Test '%s' not added to this run because"
437 " it failed verification.\n" % f
, LOG_ERR
)
440 for user
in [user
for user
in users
if len(user
)]:
441 if not verify_user(user
):
442 write_log("Not adding Test '%s' to this run.\n" %
443 self
.pathname
, LOG_ERR
)
448 def run(self
, options
):
450 Create Cmd instances for the pre/post/failsafe scripts. If the pre
451 script doesn't pass, skip this Test. Run the post script regardless.
452 If the Test is killed, also run the failsafe script.
454 odir
= os
.path
.join(self
.outputdir
, os
.path
.basename(self
.pre
))
455 pretest
= Cmd(self
.pre
, identifier
=self
.identifier
, outputdir
=odir
,
456 timeout
=self
.timeout
, user
=self
.pre_user
)
457 test
= Cmd(self
.pathname
, identifier
=self
.identifier
,
458 outputdir
=self
.outputdir
, timeout
=self
.timeout
,
460 odir
= os
.path
.join(self
.outputdir
, os
.path
.basename(self
.failsafe
))
461 failsafe
= Cmd(self
.failsafe
, identifier
=self
.identifier
,
462 outputdir
=odir
, timeout
=self
.timeout
,
463 user
=self
.failsafe_user
)
464 odir
= os
.path
.join(self
.outputdir
, os
.path
.basename(self
.post
))
465 posttest
= Cmd(self
.post
, identifier
=self
.identifier
, outputdir
=odir
,
466 timeout
=self
.timeout
, user
=self
.post_user
)
469 if len(pretest
.pathname
):
470 pretest
.run(options
.dryrun
, False)
471 cont
= pretest
.result
.result
== 'PASS'
475 test
.run(options
.dryrun
, options
.kmemleak
)
476 if test
.result
.result
== 'KILLED' and len(failsafe
.pathname
):
477 failsafe
.run(options
.dryrun
, False)
478 failsafe
.log(options
, suppress_console
=True)
484 if len(posttest
.pathname
):
485 posttest
.run(options
.dryrun
, False)
486 posttest
.log(options
)
489 class TestGroup(Test
):
490 props
= Test
.props
+ ['tests']
492 def __init__(self
, pathname
, tests
=None, **kwargs
):
493 super(TestGroup
, self
).__init
__(pathname
, **kwargs
)
494 self
.tests
= tests
or []
497 post_user
= pre_user
= failsafe_user
= ''
498 if len(self
.pre_user
):
499 pre_user
= ' (as %s)' % (self
.pre_user
)
500 if len(self
.post_user
):
501 post_user
= ' (as %s)' % (self
.post_user
)
502 if len(self
.failsafe_user
):
503 failsafe_user
= ' (as %s)' % (self
.failsafe_user
)
515 ''' % (self
.pathname
, self
.identifier
, self
.outputdir
, self
.tests
,
516 self
.timeout
, self
.user
, self
.pre
, pre_user
, self
.post
, post_user
,
517 self
.failsafe
, failsafe_user
, self
.tags
)
519 def filter(self
, keeplist
):
520 self
.tests
= [x
for x
in self
.tests
if x
in keeplist
]
524 Check the pre/post/failsafe scripts, user and tests in this TestGroup.
525 Omit the TestGroup entirely, or simply delete the relevant tests in the
526 group, if that's all that's required.
528 # If the pre/post/failsafe scripts are relative pathnames, convert to
529 # absolute, so they stand a chance of passing verification.
530 if len(self
.pre
) and not os
.path
.isabs(self
.pre
):
531 self
.pre
= os
.path
.join(self
.pathname
, self
.pre
)
532 if len(self
.post
) and not os
.path
.isabs(self
.post
):
533 self
.post
= os
.path
.join(self
.pathname
, self
.post
)
534 if len(self
.failsafe
) and not os
.path
.isabs(self
.failsafe
):
535 self
.post
= os
.path
.join(self
.pathname
, self
.post
)
537 auxfiles
= [self
.pre
, self
.post
, self
.failsafe
]
538 users
= [self
.pre_user
, self
.user
, self
.post_user
, self
.failsafe_user
]
540 for f
in [f
for f
in auxfiles
if len(f
)]:
541 if f
!= self
.failsafe
and self
.pathname
!= os
.path
.dirname(f
):
542 write_log("Warning: TestGroup '%s' not added to this run. "
543 "Auxiliary script '%s' exists in a different "
544 "directory.\n" % (self
.pathname
, f
), LOG_ERR
)
547 if not verify_file(f
):
548 write_log("Warning: TestGroup '%s' not added to this run. "
549 "Auxiliary script '%s' failed verification.\n" %
550 (self
.pathname
, f
), LOG_ERR
)
553 for user
in [user
for user
in users
if len(user
)]:
554 if not verify_user(user
):
555 write_log("Not adding TestGroup '%s' to this run.\n" %
556 self
.pathname
, LOG_ERR
)
559 # If one of the tests is invalid, delete it, log it, and drive on.
560 for test
in self
.tests
:
561 if not verify_file(os
.path
.join(self
.pathname
, test
)):
562 del self
.tests
[self
.tests
.index(test
)]
563 write_log("Warning: Test '%s' removed from TestGroup '%s' "
564 "because it failed verification.\n" %
565 (test
, self
.pathname
), LOG_ERR
)
567 return len(self
.tests
) != 0
569 def run(self
, options
):
571 Create Cmd instances for the pre/post/failsafe scripts. If the pre
572 script doesn't pass, skip all the tests in this TestGroup. Run the
573 post script regardless. Run the failsafe script when a test is killed.
575 # tags assigned to this test group also include the test names
576 if options
.tags
and not set(self
.tags
).intersection(set(options
.tags
)):
579 odir
= os
.path
.join(self
.outputdir
, os
.path
.basename(self
.pre
))
580 pretest
= Cmd(self
.pre
, outputdir
=odir
, timeout
=self
.timeout
,
581 user
=self
.pre_user
, identifier
=self
.identifier
)
582 odir
= os
.path
.join(self
.outputdir
, os
.path
.basename(self
.post
))
583 posttest
= Cmd(self
.post
, outputdir
=odir
, timeout
=self
.timeout
,
584 user
=self
.post_user
, identifier
=self
.identifier
)
587 if len(pretest
.pathname
):
588 pretest
.run(options
.dryrun
, False)
589 cont
= pretest
.result
.result
== 'PASS'
592 for fname
in self
.tests
:
593 odir
= os
.path
.join(self
.outputdir
, fname
)
594 test
= Cmd(os
.path
.join(self
.pathname
, fname
), outputdir
=odir
,
595 timeout
=self
.timeout
, user
=self
.user
,
596 identifier
=self
.identifier
)
597 odir
= os
.path
.join(odir
, os
.path
.basename(self
.failsafe
))
598 failsafe
= Cmd(self
.failsafe
, outputdir
=odir
, timeout
=self
.timeout
,
599 user
=self
.failsafe_user
, identifier
=self
.identifier
)
601 test
.run(options
.dryrun
, options
.kmemleak
)
602 if test
.result
.result
== 'KILLED' and len(failsafe
.pathname
):
603 failsafe
.run(options
.dryrun
, False)
604 failsafe
.log(options
, suppress_console
=True)
610 if len(posttest
.pathname
):
611 posttest
.run(options
.dryrun
, False)
612 posttest
.log(options
)
615 class TestRun(object):
616 props
= ['quiet', 'outputdir']
618 def __init__(self
, options
):
621 self
.starttime
= time()
622 self
.timestamp
= datetime
.now().strftime('%Y%m%dT%H%M%S')
623 self
.outputdir
= os
.path
.join(options
.outputdir
, self
.timestamp
)
624 self
.setup_logging(options
)
626 ('outputdir', BASEDIR
),
635 ('failsafe_user', ''),
640 s
= 'TestRun:\n outputdir: %s\n' % self
.outputdir
642 for key
in sorted(self
.tests
.keys()):
643 s
+= '%s%s' % (self
.tests
[key
].__str
__(), '\n')
645 for key
in sorted(self
.testgroups
.keys()):
646 s
+= '%s%s' % (self
.testgroups
[key
].__str
__(), '\n')
649 def addtest(self
, pathname
, options
):
651 Create a new Test, and apply any properties that were passed in
652 from the command line. If it passes verification, add it to the
655 test
= Test(pathname
)
656 for prop
in Test
.props
:
657 setattr(test
, prop
, getattr(options
, prop
))
660 self
.tests
[pathname
] = test
662 def addtestgroup(self
, dirname
, filenames
, options
):
664 Create a new TestGroup, and apply any properties that were passed
665 in from the command line. If it passes verification, add it to the
668 if dirname
not in self
.testgroups
:
669 testgroup
= TestGroup(dirname
)
670 for prop
in Test
.props
:
671 setattr(testgroup
, prop
, getattr(options
, prop
))
673 # Prevent pre/post/failsafe scripts from running as regular tests
674 for f
in [testgroup
.pre
, testgroup
.post
, testgroup
.failsafe
]:
676 del filenames
[filenames
.index(f
)]
678 self
.testgroups
[dirname
] = testgroup
679 self
.testgroups
[dirname
].tests
= sorted(filenames
)
683 def filter(self
, keeplist
):
684 for group
in list(self
.testgroups
.keys()):
685 if group
not in keeplist
:
686 del self
.testgroups
[group
]
689 g
= self
.testgroups
[group
]
691 if g
.pre
and os
.path
.basename(g
.pre
) in keeplist
[group
]:
694 g
.filter(keeplist
[group
])
696 for test
in list(self
.tests
.keys()):
697 directory
, base
= os
.path
.split(test
)
698 if directory
not in keeplist
or base
not in keeplist
[directory
]:
701 def read(self
, options
):
703 Read in the specified runfiles, and apply the TestRun properties
704 listed in the 'DEFAULT' section to our TestRun. Then read each
705 section, and apply the appropriate properties to the Test or
706 TestGroup. Properties from individual sections override those set
707 in the 'DEFAULT' section. If the Test or TestGroup passes
708 verification, add it to the TestRun.
710 config
= configparser
.RawConfigParser()
711 parsed
= config
.read(options
.runfiles
)
712 failed
= options
.runfiles
- set(parsed
)
714 files
= ' '.join(sorted(failed
))
715 fail("Couldn't read config files: %s" % files
)
717 for opt
in TestRun
.props
:
718 if config
.has_option('DEFAULT', opt
):
719 setattr(self
, opt
, config
.get('DEFAULT', opt
))
720 self
.outputdir
= os
.path
.join(self
.outputdir
, self
.timestamp
)
722 testdir
= options
.testdir
724 for section
in config
.sections():
725 if 'tests' in config
.options(section
):
726 parts
= section
.split(':', 1)
727 sectiondir
= parts
[0]
728 identifier
= parts
[1] if len(parts
) == 2 else None
729 if os
.path
.isdir(sectiondir
):
730 pathname
= sectiondir
731 elif os
.path
.isdir(os
.path
.join(testdir
, sectiondir
)):
732 pathname
= os
.path
.join(testdir
, sectiondir
)
734 pathname
= sectiondir
736 testgroup
= TestGroup(os
.path
.abspath(pathname
),
737 identifier
=identifier
)
738 for prop
in TestGroup
.props
:
739 for sect
in ['DEFAULT', section
]:
740 if config
.has_option(sect
, prop
):
742 setattr(testgroup
, prop
,
743 eval(config
.get(sect
, prop
)))
744 elif prop
== 'failsafe':
745 failsafe
= config
.get(sect
, prop
)
746 setattr(testgroup
, prop
,
747 os
.path
.join(testdir
, failsafe
))
749 setattr(testgroup
, prop
,
750 config
.get(sect
, prop
))
752 # Repopulate tests using eval to convert the string to a list
753 testgroup
.tests
= eval(config
.get(section
, 'tests'))
755 if testgroup
.verify():
756 self
.testgroups
[section
] = testgroup
759 for prop
in Test
.props
:
760 for sect
in ['DEFAULT', section
]:
761 if config
.has_option(sect
, prop
):
762 if prop
== 'failsafe':
763 failsafe
= config
.get(sect
, prop
)
765 os
.path
.join(testdir
, failsafe
))
767 setattr(test
, prop
, config
.get(sect
, prop
))
770 self
.tests
[section
] = test
772 def write(self
, options
):
774 Create a configuration file for editing and later use. The
775 'DEFAULT' section of the config file is created from the
776 properties that were specified on the command line. Tests are
777 simply added as sections that inherit everything from the
778 'DEFAULT' section. TestGroups are the same, except they get an
779 option including all the tests to run in that directory.
782 defaults
= dict([(prop
, getattr(options
, prop
)) for prop
, _
in
784 config
= configparser
.RawConfigParser(defaults
)
786 for test
in sorted(self
.tests
.keys()):
787 config
.add_section(test
)
788 for prop
in Test
.props
:
789 if prop
not in self
.props
:
790 config
.set(test
, prop
,
791 getattr(self
.tests
[test
], prop
))
793 for testgroup
in sorted(self
.testgroups
.keys()):
794 config
.add_section(testgroup
)
795 config
.set(testgroup
, 'tests', self
.testgroups
[testgroup
].tests
)
796 for prop
in TestGroup
.props
:
797 if prop
not in self
.props
:
798 config
.set(testgroup
, prop
,
799 getattr(self
.testgroups
[testgroup
], prop
))
802 with
open(options
.template
, 'w') as f
:
803 return config
.write(f
)
805 fail('Could not open \'%s\' for writing.' % options
.template
)
807 def complete_outputdirs(self
):
809 Collect all the pathnames for Tests, and TestGroups. Work
810 backwards one pathname component at a time, to create a unique
811 directory name in which to deposit test output. Tests will be able
812 to write output files directly in the newly modified outputdir.
813 TestGroups will be able to create one subdirectory per test in the
814 outputdir, and are guaranteed uniqueness because a group can only
815 contain files in one directory. Pre and post tests will create a
816 directory rooted at the outputdir of the Test or TestGroup in
817 question for their output. Failsafe scripts will create a directory
818 rooted at the outputdir of each Test for their output.
822 tmp_dict
= dict(list(self
.tests
.items()) +
823 list(self
.testgroups
.items()))
824 total
= len(tmp_dict
)
825 base
= self
.outputdir
830 for testfile
in list(tmp_dict
.keys()):
831 uniq
= '/'.join(testfile
.split('/')[components
:]).lstrip('/')
832 if uniq
not in paths
:
834 tmp_dict
[testfile
].outputdir
= os
.path
.join(base
, uniq
)
837 done
= total
== len(paths
)
839 def setup_logging(self
, options
):
841 This function creates the output directory and gets a file object
842 for the logfile. This function must be called before write_log()
845 if options
.dryrun
is True:
849 if not options
.template
:
852 os
.makedirs(self
.outputdir
, mode
=0o777)
854 filename
= os
.path
.join(self
.outputdir
, 'log')
855 LOG_FILE_OBJ
= open(filename
, buffering
=0, mode
='wb')
859 def run(self
, options
):
861 Walk through all the Tests and TestGroups, calling run().
864 os
.chdir(self
.outputdir
)
866 fail('Could not change to directory %s' % self
.outputdir
)
867 # make a symlink to the output for the currently running test
868 logsymlink
= os
.path
.join(self
.outputdir
, '../current')
869 if os
.path
.islink(logsymlink
):
870 os
.unlink(logsymlink
)
871 if not os
.path
.exists(logsymlink
):
872 os
.symlink(self
.outputdir
, logsymlink
)
874 write_log('Could not make a symlink to directory %s\n' %
875 self
.outputdir
, LOG_ERR
)
878 cmd
= f
'echo scan=0 | {SUDO} tee {KMEMLEAK_FILE}'
879 check_output(cmd
, shell
=True)
882 while iteration
< options
.iterations
:
883 for test
in sorted(self
.tests
.keys()):
884 self
.tests
[test
].run(options
)
885 for testgroup
in sorted(self
.testgroups
.keys()):
886 self
.testgroups
[testgroup
].run(options
)
890 if Result
.total
== 0:
893 print('\nResults Summary')
894 for key
in list(Result
.runresults
.keys()):
895 if Result
.runresults
[key
] != 0:
896 print('%s\t% 4d' % (key
, Result
.runresults
[key
]))
898 m
, s
= divmod(time() - self
.starttime
, 60)
900 print('\nRunning Time:\t%02d:%02d:%02d' % (h
, m
, s
))
901 print('Percent passed:\t%.1f%%' % ((float(Result
.runresults
['PASS']) /
902 float(Result
.total
)) * 100))
903 print('Log directory:\t%s' % self
.outputdir
)
905 if Result
.runresults
['FAIL'] > 0:
908 if Result
.runresults
['KILLED'] > 0:
911 if Result
.runresults
['RERAN'] > 0:
917 def write_log(msg
, target
):
919 Write the provided message to standard out, standard error or
920 the logfile. If specifying LOG_FILE, then `msg` must be a bytes
921 like object. This way we can still handle output from tests that
922 may be in unexpected encodings.
924 if target
== LOG_OUT
:
925 os
.write(sys
.stdout
.fileno(), bytearray(msg
, encoding
='utf-8'))
926 elif target
== LOG_ERR
:
927 os
.write(sys
.stderr
.fileno(), bytearray(msg
, encoding
='utf-8'))
928 elif target
== LOG_FILE
:
929 os
.write(LOG_FILE_OBJ
.fileno(), msg
)
931 fail('log_msg called with unknown target "%s"' % target
)
934 def verify_file(pathname
):
936 Verify that the supplied pathname is an executable regular file.
938 if os
.path
.isdir(pathname
) or os
.path
.islink(pathname
):
941 for ext
in '', '.ksh', '.sh':
942 script_path
= pathname
+ ext
943 if os
.path
.isfile(script_path
) and os
.access(script_path
, os
.X_OK
):
949 def verify_user(user
):
951 Verify that the specified user exists on this system, and can execute
952 sudo without being prompted for a password.
954 testcmd
= [SUDO
, '-n', '-u', user
, TRUE
]
956 if user
in Cmd
.verified_users
:
962 write_log("Warning: user '%s' does not exist.\n" % user
,
968 if p
.returncode
!= 0:
969 write_log("Warning: user '%s' cannot use passwordless sudo.\n" % user
,
973 Cmd
.verified_users
.append(user
)
978 def find_tests(testrun
, options
):
980 For the given list of pathnames, add files as Tests. For directories,
981 if do_groups is True, add the directory as a TestGroup. If False,
982 recursively search for executable files.
985 for p
in sorted(options
.pathnames
):
987 for dirname
, _
, filenames
in os
.walk(p
):
988 if options
.do_groups
:
989 testrun
.addtestgroup(dirname
, filenames
, options
)
991 for f
in sorted(filenames
):
992 testrun
.addtest(os
.path
.join(dirname
, f
), options
)
994 testrun
.addtest(p
, options
)
997 def filter_tests(testrun
, options
):
999 fh
= open(options
.logfile
, "r")
1000 except Exception as e
:
1005 line
= fh
.readline()
1008 m
= re
.match(r
'Test: .*(tests/.*)/(\S+).*\[FAIL\]', line
)
1011 group
, test
= m
.group(1, 2)
1013 failed
[group
].append(test
)
1015 failed
[group
] = [test
]
1018 testrun
.filter(failed
)
1021 def fail(retstr
, ret
=1):
1022 print('%s: %s' % (sys
.argv
[0], retstr
))
1026 def kmemleak_cb(option
, opt_str
, value
, parser
):
1027 if not os
.path
.exists(KMEMLEAK_FILE
):
1028 fail(f
"File '{KMEMLEAK_FILE}' doesn't exist. " +
1029 "Enable CONFIG_DEBUG_KMEMLEAK in kernel configuration.")
1031 setattr(parser
.values
, option
.dest
, True)
1034 def options_cb(option
, opt_str
, value
, parser
):
1035 path_options
= ['outputdir', 'template', 'testdir', 'logfile']
1037 if opt_str
in parser
.rargs
:
1038 fail('%s may only be specified once.' % opt_str
)
1040 if option
.dest
== 'runfiles':
1041 parser
.values
.cmd
= 'rdconfig'
1042 value
= set(os
.path
.abspath(p
) for p
in value
.split(','))
1043 if option
.dest
== 'tags':
1044 value
= [x
.strip() for x
in value
.split(',')]
1046 if option
.dest
in path_options
:
1047 setattr(parser
.values
, option
.dest
, os
.path
.abspath(value
))
1049 setattr(parser
.values
, option
.dest
, value
)
1053 parser
= OptionParser()
1054 parser
.add_option('-c', action
='callback', callback
=options_cb
,
1055 type='string', dest
='runfiles', metavar
='runfiles',
1056 help='Specify tests to run via config files.')
1057 parser
.add_option('-d', action
='store_true', default
=False, dest
='dryrun',
1058 help='Dry run. Print tests, but take no other action.')
1059 parser
.add_option('-l', action
='callback', callback
=options_cb
,
1060 default
=None, dest
='logfile', metavar
='logfile',
1062 help='Read logfile and re-run tests which failed.')
1063 parser
.add_option('-g', action
='store_true', default
=False,
1064 dest
='do_groups', help='Make directories TestGroups.')
1065 parser
.add_option('-o', action
='callback', callback
=options_cb
,
1066 default
=BASEDIR
, dest
='outputdir', type='string',
1067 metavar
='outputdir', help='Specify an output directory.')
1068 parser
.add_option('-i', action
='callback', callback
=options_cb
,
1069 default
=TESTDIR
, dest
='testdir', type='string',
1070 metavar
='testdir', help='Specify a test directory.')
1071 parser
.add_option('-m', action
='callback', callback
=kmemleak_cb
,
1072 default
=False, dest
='kmemleak',
1073 help='Enable kmemleak reporting (Linux only)')
1074 parser
.add_option('-p', action
='callback', callback
=options_cb
,
1075 default
='', dest
='pre', metavar
='script',
1076 type='string', help='Specify a pre script.')
1077 parser
.add_option('-P', action
='callback', callback
=options_cb
,
1078 default
='', dest
='post', metavar
='script',
1079 type='string', help='Specify a post script.')
1080 parser
.add_option('-q', action
='store_true', default
=False, dest
='quiet',
1081 help='Silence on the console during a test run.')
1082 parser
.add_option('-s', action
='callback', callback
=options_cb
,
1083 default
='', dest
='failsafe', metavar
='script',
1084 type='string', help='Specify a failsafe script.')
1085 parser
.add_option('-S', action
='callback', callback
=options_cb
,
1086 default
='', dest
='failsafe_user',
1087 metavar
='failsafe_user', type='string',
1088 help='Specify a user to execute the failsafe script.')
1089 parser
.add_option('-t', action
='callback', callback
=options_cb
, default
=60,
1090 dest
='timeout', metavar
='seconds', type='int',
1091 help='Timeout (in seconds) for an individual test.')
1092 parser
.add_option('-u', action
='callback', callback
=options_cb
,
1093 default
='', dest
='user', metavar
='user', type='string',
1094 help='Specify a different user name to run as.')
1095 parser
.add_option('-w', action
='callback', callback
=options_cb
,
1096 default
=None, dest
='template', metavar
='template',
1097 type='string', help='Create a new config file.')
1098 parser
.add_option('-x', action
='callback', callback
=options_cb
, default
='',
1099 dest
='pre_user', metavar
='pre_user', type='string',
1100 help='Specify a user to execute the pre script.')
1101 parser
.add_option('-X', action
='callback', callback
=options_cb
, default
='',
1102 dest
='post_user', metavar
='post_user', type='string',
1103 help='Specify a user to execute the post script.')
1104 parser
.add_option('-T', action
='callback', callback
=options_cb
, default
='',
1105 dest
='tags', metavar
='tags', type='string',
1106 help='Specify tags to execute specific test groups.')
1107 parser
.add_option('-I', action
='callback', callback
=options_cb
, default
=1,
1108 dest
='iterations', metavar
='iterations', type='int',
1109 help='Number of times to run the test run.')
1110 (options
, pathnames
) = parser
.parse_args()
1112 if options
.runfiles
and len(pathnames
):
1113 fail('Extraneous arguments.')
1115 options
.pathnames
= [os
.path
.abspath(path
) for path
in pathnames
]
1121 options
= parse_args()
1123 testrun
= TestRun(options
)
1125 if options
.runfiles
:
1126 testrun
.read(options
)
1128 find_tests(testrun
, options
)
1131 filter_tests(testrun
, options
)
1133 if options
.template
:
1134 testrun
.write(options
)
1137 testrun
.complete_outputdirs()
1138 testrun
.run(options
)
1139 exit(testrun
.summary())
1142 if __name__
== '__main__':