]>
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 threading
import Timer
42 BASEDIR
= '/var/tmp/test_results'
43 TESTDIR
= '/usr/share/zfs/'
52 # some python 2.7 system don't have a concept of monotonic time
53 CLOCK_MONOTONIC_RAW
= 4 # see <linux/time.h>
56 class timespec(ctypes
.Structure
):
58 ('tv_sec', ctypes
.c_long
),
59 ('tv_nsec', ctypes
.c_long
)
63 librt
= ctypes
.CDLL('librt.so.1', use_errno
=True)
64 clock_gettime
= librt
.clock_gettime
65 clock_gettime
.argtypes
= [ctypes
.c_int
, ctypes
.POINTER(timespec
)]
70 if clock_gettime(CLOCK_MONOTONIC_RAW
, ctypes
.pointer(t
)) != 0:
71 errno_
= ctypes
.get_errno()
72 raise OSError(errno_
, os
.strerror(errno_
))
73 return t
.tv_sec
+ t
.tv_nsec
* 1e-9
78 runresults
= {'PASS': 0, 'FAIL': 0, 'SKIP': 0, 'KILLED': 0, 'RERAN': 0}
82 self
.returncode
= None
88 def done(self
, proc
, killed
, reran
):
90 Finalize the results of this Cmd.
93 m
, s
= divmod(monotonic_time() - self
.starttime
, 60)
94 self
.runtime
= '%02d:%02d' % (m
, s
)
95 self
.returncode
= proc
.returncode
97 Result
.runresults
['RERAN'] += 1
99 self
.result
= 'KILLED'
100 Result
.runresults
['KILLED'] += 1
101 elif self
.returncode
== 0:
103 Result
.runresults
['PASS'] += 1
104 elif self
.returncode
== 4:
106 Result
.runresults
['SKIP'] += 1
107 elif self
.returncode
!= 0:
109 Result
.runresults
['FAIL'] += 1
112 class Output(object):
114 This class is a slightly modified version of the 'Stream' class found
115 here: http://goo.gl/aSGfv
117 def __init__(self
, stream
):
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)
148 buf
= self
._buf
+ buf
149 tmp
, rest
= buf
.rsplit(b
'\n', 1)
152 rows
= tmp
.split(b
'\n')
153 self
.lines
+= [(now
, r
) for r
in rows
]
159 def __init__(self
, pathname
, identifier
=None, outputdir
=None,
160 timeout
=None, user
=None, tags
=None):
161 self
.pathname
= pathname
162 self
.identifier
= identifier
163 self
.outputdir
= outputdir
or 'BASEDIR'
165 The timeout for tests is measured in wall-clock time
167 self
.timeout
= timeout
168 self
.user
= user
or ''
171 self
.result
= Result()
173 if self
.timeout
is None:
183 ''' % (self
.pathname
, self
.identifier
, self
.outputdir
, self
.timeout
, self
.user
)
185 def kill_cmd(self
, proc
, keyboard_interrupt
=False):
187 Kill a running command due to timeout, or ^C from the keyboard. If
188 sudo is required, this user was verified previously.
191 do_sudo
= len(self
.user
) != 0
194 cmd
= [SUDO
, KILL
, signal
, str(proc
.pid
)]
205 If this is not a user-initiated kill and the test has not been
206 reran before we consider if the test needs to be reran:
207 If the test has spent some time hibernating and didn't run the whole
208 length of time before being timed out we will rerun the test.
210 if keyboard_interrupt
is False and self
.reran
is None:
211 runtime
= monotonic_time() - self
.result
.starttime
212 if int(self
.timeout
) > runtime
:
218 def update_cmd_privs(self
, cmd
, user
):
220 If a user has been specified to run this Cmd and we're not already
221 running as that user, prepend the appropriate sudo command to run
224 me
= getpwuid(os
.getuid())
226 if not user
or user
is me
:
227 if os
.path
.isfile(cmd
+'.ksh') and os
.access(cmd
+'.ksh', os
.X_OK
):
229 if os
.path
.isfile(cmd
+'.sh') and os
.access(cmd
+'.sh', os
.X_OK
):
233 if not os
.path
.isfile(cmd
):
234 if os
.path
.isfile(cmd
+'.ksh') and os
.access(cmd
+'.ksh', os
.X_OK
):
236 if os
.path
.isfile(cmd
+'.sh') and os
.access(cmd
+'.sh', os
.X_OK
):
239 ret
= '%s -E -u %s %s' % (SUDO
, user
, cmd
)
240 return ret
.split(' ')
242 def collect_output(self
, proc
):
244 Read from stdout/stderr as data becomes available, until the
245 process is no longer running. Return the lines from the stdout and
246 stderr Output objects.
248 out
= Output(proc
.stdout
)
249 err
= Output(proc
.stderr
)
251 while proc
.returncode
is None:
253 res
= select([out
, err
], [], [], .1)
259 return out
.lines
, err
.lines
261 def run(self
, dryrun
):
263 This is the main function that runs each individual test.
264 Determine whether or not the command requires sudo, and modify it
265 if needed. Run the command, and update the result object.
271 privcmd
= self
.update_cmd_privs(self
.pathname
, self
.user
)
274 if not os
.path
.isdir(self
.outputdir
):
275 os
.makedirs(self
.outputdir
, mode
=0o777)
280 self
.result
.starttime
= monotonic_time()
281 proc
= Popen(privcmd
, stdout
=PIPE
, stderr
=PIPE
)
282 # Allow a special timeout value of 0 to mean infinity
283 if int(self
.timeout
) == 0:
284 self
.timeout
= sys
.maxsize
285 t
= Timer(int(self
.timeout
), self
.kill_cmd
, [proc
])
289 self
.result
.stdout
, self
.result
.stderr
= self
.collect_output(proc
)
290 except KeyboardInterrupt:
291 self
.kill_cmd(proc
, True)
292 fail('\nRun terminated at user request.')
296 if self
.reran
is not False:
297 self
.result
.done(proc
, self
.killed
, self
.reran
)
301 Initialize enough of the test result that we can log a skipped
305 Result
.runresults
['SKIP'] += 1
306 self
.result
.stdout
= self
.result
.stderr
= []
307 self
.result
.starttime
= monotonic_time()
308 m
, s
= divmod(monotonic_time() - self
.result
.starttime
, 60)
309 self
.result
.runtime
= '%02d:%02d' % (m
, s
)
310 self
.result
.result
= 'SKIP'
312 def log(self
, options
, suppress_console
=False):
314 This function is responsible for writing all output. This includes
315 the console output, the logfile of all results (with timestamped
316 merged stdout and stderr), and for each test, the unmodified
317 stdout/stderr/merged in its own file.
320 logname
= getpwuid(os
.getuid()).pw_name
322 if self
.reran
is True:
324 user
= ' (run as %s)' % (self
.user
if len(self
.user
) else logname
)
326 msga
= 'Test (%s): %s%s ' % (self
.identifier
, self
.pathname
, user
)
328 msga
= 'Test: %s%s ' % (self
.pathname
, user
)
329 msgb
= '[%s] [%s]%s\n' % (self
.result
.runtime
, self
.result
.result
, rer
)
330 pad
= ' ' * (80 - (len(msga
) + len(msgb
)))
331 result_line
= msga
+ pad
+ msgb
333 # The result line is always written to the log file. If -q was
334 # specified only failures are written to the console, otherwise
335 # the result line is written to the console. The console output
336 # may be suppressed by calling log() with suppress_console=True.
337 write_log(bytearray(result_line
, encoding
='utf-8'), LOG_FILE
)
338 if not suppress_console
:
339 if not options
.quiet
:
340 write_log(result_line
, LOG_OUT
)
341 elif options
.quiet
and self
.result
.result
!= 'PASS':
342 write_log(result_line
, LOG_OUT
)
344 lines
= sorted(self
.result
.stdout
+ self
.result
.stderr
,
347 # Write timestamped output (stdout and stderr) to the logfile
348 for dt
, line
in lines
:
349 timestamp
= bytearray(dt
.strftime("%H:%M:%S.%f ")[:11],
351 write_log(b
'%s %s\n' % (timestamp
, line
), LOG_FILE
)
353 # Write the separate stdout/stderr/merged files, if the data exists
354 if len(self
.result
.stdout
):
355 with
open(os
.path
.join(self
.outputdir
, 'stdout'), 'wb') as out
:
356 for _
, line
in self
.result
.stdout
:
357 os
.write(out
.fileno(), b
'%s\n' % line
)
358 if len(self
.result
.stderr
):
359 with
open(os
.path
.join(self
.outputdir
, 'stderr'), 'wb') as err
:
360 for _
, line
in self
.result
.stderr
:
361 os
.write(err
.fileno(), b
'%s\n' % line
)
362 if len(self
.result
.stdout
) and len(self
.result
.stderr
):
363 with
open(os
.path
.join(self
.outputdir
, 'merged'), 'wb') as merged
:
364 for _
, line
in lines
:
365 os
.write(merged
.fileno(), b
'%s\n' % line
)
369 props
= ['outputdir', 'timeout', 'user', 'pre', 'pre_user', 'post',
370 'post_user', 'failsafe', 'failsafe_user', 'tags']
372 def __init__(self
, pathname
,
373 pre
=None, pre_user
=None, post
=None, post_user
=None,
374 failsafe
=None, failsafe_user
=None, tags
=None, **kwargs
):
375 super(Test
, self
).__init
__(pathname
, **kwargs
)
377 self
.pre_user
= pre_user
or ''
378 self
.post
= post
or ''
379 self
.post_user
= post_user
or ''
380 self
.failsafe
= failsafe
or ''
381 self
.failsafe_user
= failsafe_user
or ''
382 self
.tags
= tags
or []
385 post_user
= pre_user
= failsafe_user
= ''
386 if len(self
.pre_user
):
387 pre_user
= ' (as %s)' % (self
.pre_user
)
388 if len(self
.post_user
):
389 post_user
= ' (as %s)' % (self
.post_user
)
390 if len(self
.failsafe_user
):
391 failsafe_user
= ' (as %s)' % (self
.failsafe_user
)
402 ''' % (self
.pathname
, self
.identifier
, self
.outputdir
, self
.timeout
, self
.user
,
403 self
.pre
, pre_user
, self
.post
, post_user
, self
.failsafe
,
404 failsafe_user
, self
.tags
)
408 Check the pre/post/failsafe scripts, user and Test. Omit the Test from
409 this run if there are any problems.
411 files
= [self
.pre
, self
.pathname
, self
.post
, self
.failsafe
]
412 users
= [self
.pre_user
, self
.user
, self
.post_user
, self
.failsafe_user
]
414 for f
in [f
for f
in files
if len(f
)]:
415 if not verify_file(f
):
416 write_log("Warning: Test '%s' not added to this run because"
417 " it failed verification.\n" % f
, LOG_ERR
)
420 for user
in [user
for user
in users
if len(user
)]:
421 if not verify_user(user
):
422 write_log("Not adding Test '%s' to this run.\n" %
423 self
.pathname
, LOG_ERR
)
428 def run(self
, options
):
430 Create Cmd instances for the pre/post/failsafe scripts. If the pre
431 script doesn't pass, skip this Test. Run the post script regardless.
432 If the Test is killed, also run the failsafe script.
434 odir
= os
.path
.join(self
.outputdir
, os
.path
.basename(self
.pre
))
435 pretest
= Cmd(self
.pre
, identifier
=self
.identifier
, outputdir
=odir
,
436 timeout
=self
.timeout
, user
=self
.pre_user
)
437 test
= Cmd(self
.pathname
, identifier
=self
.identifier
,
438 outputdir
=self
.outputdir
, timeout
=self
.timeout
,
440 odir
= os
.path
.join(self
.outputdir
, os
.path
.basename(self
.failsafe
))
441 failsafe
= Cmd(self
.failsafe
, identifier
=self
.identifier
,
442 outputdir
=odir
, timeout
=self
.timeout
,
443 user
=self
.failsafe_user
)
444 odir
= os
.path
.join(self
.outputdir
, os
.path
.basename(self
.post
))
445 posttest
= Cmd(self
.post
, identifier
=self
.identifier
, outputdir
=odir
,
446 timeout
=self
.timeout
, user
=self
.post_user
)
449 if len(pretest
.pathname
):
450 pretest
.run(options
.dryrun
)
451 cont
= pretest
.result
.result
== 'PASS'
455 test
.run(options
.dryrun
)
456 if test
.result
.result
== 'KILLED' and len(failsafe
.pathname
):
457 failsafe
.run(options
.dryrun
)
458 failsafe
.log(options
, suppress_console
=True)
464 if len(posttest
.pathname
):
465 posttest
.run(options
.dryrun
)
466 posttest
.log(options
)
469 class TestGroup(Test
):
470 props
= Test
.props
+ ['tests']
472 def __init__(self
, pathname
, tests
=None, **kwargs
):
473 super(TestGroup
, self
).__init
__(pathname
, **kwargs
)
474 self
.tests
= tests
or []
477 post_user
= pre_user
= failsafe_user
= ''
478 if len(self
.pre_user
):
479 pre_user
= ' (as %s)' % (self
.pre_user
)
480 if len(self
.post_user
):
481 post_user
= ' (as %s)' % (self
.post_user
)
482 if len(self
.failsafe_user
):
483 failsafe_user
= ' (as %s)' % (self
.failsafe_user
)
495 ''' % (self
.pathname
, self
.identifier
, self
.outputdir
, self
.tests
,
496 self
.timeout
, self
.user
, self
.pre
, pre_user
, self
.post
, post_user
,
497 self
.failsafe
, failsafe_user
, self
.tags
)
499 def filter(self
, keeplist
):
500 self
.tests
= [x
for x
in self
.tests
if x
in keeplist
]
504 Check the pre/post/failsafe scripts, user and tests in this TestGroup.
505 Omit the TestGroup entirely, or simply delete the relevant tests in the
506 group, if that's all that's required.
508 # If the pre/post/failsafe scripts are relative pathnames, convert to
509 # absolute, so they stand a chance of passing verification.
510 if len(self
.pre
) and not os
.path
.isabs(self
.pre
):
511 self
.pre
= os
.path
.join(self
.pathname
, self
.pre
)
512 if len(self
.post
) and not os
.path
.isabs(self
.post
):
513 self
.post
= os
.path
.join(self
.pathname
, self
.post
)
514 if len(self
.failsafe
) and not os
.path
.isabs(self
.failsafe
):
515 self
.post
= os
.path
.join(self
.pathname
, self
.post
)
517 auxfiles
= [self
.pre
, self
.post
, self
.failsafe
]
518 users
= [self
.pre_user
, self
.user
, self
.post_user
, self
.failsafe_user
]
520 for f
in [f
for f
in auxfiles
if len(f
)]:
521 if f
!= self
.failsafe
and self
.pathname
!= os
.path
.dirname(f
):
522 write_log("Warning: TestGroup '%s' not added to this run. "
523 "Auxiliary script '%s' exists in a different "
524 "directory.\n" % (self
.pathname
, f
), LOG_ERR
)
527 if not verify_file(f
):
528 write_log("Warning: TestGroup '%s' not added to this run. "
529 "Auxiliary script '%s' failed verification.\n" %
530 (self
.pathname
, f
), LOG_ERR
)
533 for user
in [user
for user
in users
if len(user
)]:
534 if not verify_user(user
):
535 write_log("Not adding TestGroup '%s' to this run.\n" %
536 self
.pathname
, LOG_ERR
)
539 # If one of the tests is invalid, delete it, log it, and drive on.
540 for test
in self
.tests
:
541 if not verify_file(os
.path
.join(self
.pathname
, test
)):
542 del self
.tests
[self
.tests
.index(test
)]
543 write_log("Warning: Test '%s' removed from TestGroup '%s' "
544 "because it failed verification.\n" %
545 (test
, self
.pathname
), LOG_ERR
)
547 return len(self
.tests
) != 0
549 def run(self
, options
):
551 Create Cmd instances for the pre/post/failsafe scripts. If the pre
552 script doesn't pass, skip all the tests in this TestGroup. Run the
553 post script regardless. Run the failsafe script when a test is killed.
555 # tags assigned to this test group also include the test names
556 if options
.tags
and not set(self
.tags
).intersection(set(options
.tags
)):
559 odir
= os
.path
.join(self
.outputdir
, os
.path
.basename(self
.pre
))
560 pretest
= Cmd(self
.pre
, outputdir
=odir
, timeout
=self
.timeout
,
561 user
=self
.pre_user
, identifier
=self
.identifier
)
562 odir
= os
.path
.join(self
.outputdir
, os
.path
.basename(self
.post
))
563 posttest
= Cmd(self
.post
, outputdir
=odir
, timeout
=self
.timeout
,
564 user
=self
.post_user
, identifier
=self
.identifier
)
567 if len(pretest
.pathname
):
568 pretest
.run(options
.dryrun
)
569 cont
= pretest
.result
.result
== 'PASS'
572 for fname
in self
.tests
:
573 odir
= os
.path
.join(self
.outputdir
, fname
)
574 test
= Cmd(os
.path
.join(self
.pathname
, fname
), outputdir
=odir
,
575 timeout
=self
.timeout
, user
=self
.user
,
576 identifier
=self
.identifier
)
577 odir
= os
.path
.join(odir
, os
.path
.basename(self
.failsafe
))
578 failsafe
= Cmd(self
.failsafe
, outputdir
=odir
, timeout
=self
.timeout
,
579 user
=self
.failsafe_user
, identifier
=self
.identifier
)
581 test
.run(options
.dryrun
)
582 if test
.result
.result
== 'KILLED' and len(failsafe
.pathname
):
583 failsafe
.run(options
.dryrun
)
584 failsafe
.log(options
, suppress_console
=True)
590 if len(posttest
.pathname
):
591 posttest
.run(options
.dryrun
)
592 posttest
.log(options
)
595 class TestRun(object):
596 props
= ['quiet', 'outputdir']
598 def __init__(self
, options
):
601 self
.starttime
= time()
602 self
.timestamp
= datetime
.now().strftime('%Y%m%dT%H%M%S')
603 self
.outputdir
= os
.path
.join(options
.outputdir
, self
.timestamp
)
604 self
.setup_logging(options
)
606 ('outputdir', BASEDIR
),
615 ('failsafe_user', ''),
620 s
= 'TestRun:\n outputdir: %s\n' % self
.outputdir
622 for key
in sorted(self
.tests
.keys()):
623 s
+= '%s%s' % (self
.tests
[key
].__str
__(), '\n')
625 for key
in sorted(self
.testgroups
.keys()):
626 s
+= '%s%s' % (self
.testgroups
[key
].__str
__(), '\n')
629 def addtest(self
, pathname
, options
):
631 Create a new Test, and apply any properties that were passed in
632 from the command line. If it passes verification, add it to the
635 test
= Test(pathname
)
636 for prop
in Test
.props
:
637 setattr(test
, prop
, getattr(options
, prop
))
640 self
.tests
[pathname
] = test
642 def addtestgroup(self
, dirname
, filenames
, options
):
644 Create a new TestGroup, and apply any properties that were passed
645 in from the command line. If it passes verification, add it to the
648 if dirname
not in self
.testgroups
:
649 testgroup
= TestGroup(dirname
)
650 for prop
in Test
.props
:
651 setattr(testgroup
, prop
, getattr(options
, prop
))
653 # Prevent pre/post/failsafe scripts from running as regular tests
654 for f
in [testgroup
.pre
, testgroup
.post
, testgroup
.failsafe
]:
656 del filenames
[filenames
.index(f
)]
658 self
.testgroups
[dirname
] = testgroup
659 self
.testgroups
[dirname
].tests
= sorted(filenames
)
663 def filter(self
, keeplist
):
664 for group
in list(self
.testgroups
.keys()):
665 if group
not in keeplist
:
666 del self
.testgroups
[group
]
669 g
= self
.testgroups
[group
]
671 if g
.pre
and os
.path
.basename(g
.pre
) in keeplist
[group
]:
674 g
.filter(keeplist
[group
])
676 for test
in list(self
.tests
.keys()):
677 directory
, base
= os
.path
.split(test
)
678 if directory
not in keeplist
or base
not in keeplist
[directory
]:
681 def read(self
, options
):
683 Read in the specified runfiles, and apply the TestRun properties
684 listed in the 'DEFAULT' section to our TestRun. Then read each
685 section, and apply the appropriate properties to the Test or
686 TestGroup. Properties from individual sections override those set
687 in the 'DEFAULT' section. If the Test or TestGroup passes
688 verification, add it to the TestRun.
690 config
= configparser
.RawConfigParser()
691 parsed
= config
.read(options
.runfiles
)
692 failed
= options
.runfiles
- set(parsed
)
694 files
= ' '.join(sorted(failed
))
695 fail("Couldn't read config files: %s" % files
)
697 for opt
in TestRun
.props
:
698 if config
.has_option('DEFAULT', opt
):
699 setattr(self
, opt
, config
.get('DEFAULT', opt
))
700 self
.outputdir
= os
.path
.join(self
.outputdir
, self
.timestamp
)
702 testdir
= options
.testdir
704 for section
in config
.sections():
705 if 'tests' in config
.options(section
):
706 parts
= section
.split(':', 1)
707 sectiondir
= parts
[0]
708 identifier
= parts
[1] if len(parts
) == 2 else None
709 if os
.path
.isdir(sectiondir
):
710 pathname
= sectiondir
711 elif os
.path
.isdir(os
.path
.join(testdir
, sectiondir
)):
712 pathname
= os
.path
.join(testdir
, sectiondir
)
714 pathname
= sectiondir
716 testgroup
= TestGroup(os
.path
.abspath(pathname
),
717 identifier
=identifier
)
718 for prop
in TestGroup
.props
:
719 for sect
in ['DEFAULT', section
]:
720 if config
.has_option(sect
, prop
):
722 setattr(testgroup
, prop
,
723 eval(config
.get(sect
, prop
)))
724 elif prop
== 'failsafe':
725 failsafe
= config
.get(sect
, prop
)
726 setattr(testgroup
, prop
,
727 os
.path
.join(testdir
, failsafe
))
729 setattr(testgroup
, prop
,
730 config
.get(sect
, prop
))
732 # Repopulate tests using eval to convert the string to a list
733 testgroup
.tests
= eval(config
.get(section
, 'tests'))
735 if testgroup
.verify():
736 self
.testgroups
[section
] = testgroup
739 for prop
in Test
.props
:
740 for sect
in ['DEFAULT', section
]:
741 if config
.has_option(sect
, prop
):
742 if prop
== 'failsafe':
743 failsafe
= config
.get(sect
, prop
)
745 os
.path
.join(testdir
, failsafe
))
747 setattr(test
, prop
, config
.get(sect
, prop
))
750 self
.tests
[section
] = test
752 def write(self
, options
):
754 Create a configuration file for editing and later use. The
755 'DEFAULT' section of the config file is created from the
756 properties that were specified on the command line. Tests are
757 simply added as sections that inherit everything from the
758 'DEFAULT' section. TestGroups are the same, except they get an
759 option including all the tests to run in that directory.
762 defaults
= dict([(prop
, getattr(options
, prop
)) for prop
, _
in
764 config
= configparser
.RawConfigParser(defaults
)
766 for test
in sorted(self
.tests
.keys()):
767 config
.add_section(test
)
768 for prop
in Test
.props
:
769 if prop
not in self
.props
:
770 config
.set(test
, prop
,
771 getattr(self
.tests
[test
], prop
))
773 for testgroup
in sorted(self
.testgroups
.keys()):
774 config
.add_section(testgroup
)
775 config
.set(testgroup
, 'tests', self
.testgroups
[testgroup
].tests
)
776 for prop
in TestGroup
.props
:
777 if prop
not in self
.props
:
778 config
.set(testgroup
, prop
,
779 getattr(self
.testgroups
[testgroup
], prop
))
782 with
open(options
.template
, 'w') as f
:
783 return config
.write(f
)
785 fail('Could not open \'%s\' for writing.' % options
.template
)
787 def complete_outputdirs(self
):
789 Collect all the pathnames for Tests, and TestGroups. Work
790 backwards one pathname component at a time, to create a unique
791 directory name in which to deposit test output. Tests will be able
792 to write output files directly in the newly modified outputdir.
793 TestGroups will be able to create one subdirectory per test in the
794 outputdir, and are guaranteed uniqueness because a group can only
795 contain files in one directory. Pre and post tests will create a
796 directory rooted at the outputdir of the Test or TestGroup in
797 question for their output. Failsafe scripts will create a directory
798 rooted at the outputdir of each Test for their output.
802 tmp_dict
= dict(list(self
.tests
.items()) +
803 list(self
.testgroups
.items()))
804 total
= len(tmp_dict
)
805 base
= self
.outputdir
810 for testfile
in list(tmp_dict
.keys()):
811 uniq
= '/'.join(testfile
.split('/')[components
:]).lstrip('/')
812 if uniq
not in paths
:
814 tmp_dict
[testfile
].outputdir
= os
.path
.join(base
, uniq
)
817 done
= total
== len(paths
)
819 def setup_logging(self
, options
):
821 This function creates the output directory and gets a file object
822 for the logfile. This function must be called before write_log()
825 if options
.dryrun
is True:
829 if not options
.template
:
832 os
.makedirs(self
.outputdir
, mode
=0o777)
834 filename
= os
.path
.join(self
.outputdir
, 'log')
835 LOG_FILE_OBJ
= open(filename
, buffering
=0, mode
='wb')
839 def run(self
, options
):
841 Walk through all the Tests and TestGroups, calling run().
844 os
.chdir(self
.outputdir
)
846 fail('Could not change to directory %s' % self
.outputdir
)
847 # make a symlink to the output for the currently running test
848 logsymlink
= os
.path
.join(self
.outputdir
, '../current')
849 if os
.path
.islink(logsymlink
):
850 os
.unlink(logsymlink
)
851 if not os
.path
.exists(logsymlink
):
852 os
.symlink(self
.outputdir
, logsymlink
)
854 write_log('Could not make a symlink to directory %s\n' %
855 self
.outputdir
, LOG_ERR
)
857 while iteration
< options
.iterations
:
858 for test
in sorted(self
.tests
.keys()):
859 self
.tests
[test
].run(options
)
860 for testgroup
in sorted(self
.testgroups
.keys()):
861 self
.testgroups
[testgroup
].run(options
)
865 if Result
.total
== 0:
868 print('\nResults Summary')
869 for key
in list(Result
.runresults
.keys()):
870 if Result
.runresults
[key
] != 0:
871 print('%s\t% 4d' % (key
, Result
.runresults
[key
]))
873 m
, s
= divmod(time() - self
.starttime
, 60)
875 print('\nRunning Time:\t%02d:%02d:%02d' % (h
, m
, s
))
876 print('Percent passed:\t%.1f%%' % ((float(Result
.runresults
['PASS']) /
877 float(Result
.total
)) * 100))
878 print('Log directory:\t%s' % self
.outputdir
)
880 if Result
.runresults
['FAIL'] > 0:
883 if Result
.runresults
['KILLED'] > 0:
886 if Result
.runresults
['RERAN'] > 0:
892 def write_log(msg
, target
):
894 Write the provided message to standard out, standard error or
895 the logfile. If specifying LOG_FILE, then `msg` must be a bytes
896 like object. This way we can still handle output from tests that
897 may be in unexpected encodings.
899 if target
== LOG_OUT
:
900 os
.write(sys
.stdout
.fileno(), bytearray(msg
, encoding
='utf-8'))
901 elif target
== LOG_ERR
:
902 os
.write(sys
.stderr
.fileno(), bytearray(msg
, encoding
='utf-8'))
903 elif target
== LOG_FILE
:
904 os
.write(LOG_FILE_OBJ
.fileno(), msg
)
906 fail('log_msg called with unknown target "%s"' % target
)
909 def verify_file(pathname
):
911 Verify that the supplied pathname is an executable regular file.
913 if os
.path
.isdir(pathname
) or os
.path
.islink(pathname
):
916 for ext
in '', '.ksh', '.sh':
917 script_path
= pathname
+ ext
918 if os
.path
.isfile(script_path
) and os
.access(script_path
, os
.X_OK
):
924 def verify_user(user
):
926 Verify that the specified user exists on this system, and can execute
927 sudo without being prompted for a password.
929 testcmd
= [SUDO
, '-n', '-u', user
, TRUE
]
931 if user
in Cmd
.verified_users
:
937 write_log("Warning: user '%s' does not exist.\n" % user
,
943 if p
.returncode
!= 0:
944 write_log("Warning: user '%s' cannot use passwordless sudo.\n" % user
,
948 Cmd
.verified_users
.append(user
)
953 def find_tests(testrun
, options
):
955 For the given list of pathnames, add files as Tests. For directories,
956 if do_groups is True, add the directory as a TestGroup. If False,
957 recursively search for executable files.
960 for p
in sorted(options
.pathnames
):
962 for dirname
, _
, filenames
in os
.walk(p
):
963 if options
.do_groups
:
964 testrun
.addtestgroup(dirname
, filenames
, options
)
966 for f
in sorted(filenames
):
967 testrun
.addtest(os
.path
.join(dirname
, f
), options
)
969 testrun
.addtest(p
, options
)
972 def filter_tests(testrun
, options
):
974 fh
= open(options
.logfile
, "r")
975 except Exception as e
:
983 m
= re
.match(r
'Test: .*(tests/.*)/(\S+).*\[FAIL\]', line
)
986 group
, test
= m
.group(1, 2)
988 failed
[group
].append(test
)
990 failed
[group
] = [test
]
993 testrun
.filter(failed
)
996 def fail(retstr
, ret
=1):
997 print('%s: %s' % (sys
.argv
[0], retstr
))
1001 def options_cb(option
, opt_str
, value
, parser
):
1002 path_options
= ['outputdir', 'template', 'testdir', 'logfile']
1004 if opt_str
in parser
.rargs
:
1005 fail('%s may only be specified once.' % opt_str
)
1007 if option
.dest
== 'runfiles':
1008 parser
.values
.cmd
= 'rdconfig'
1009 value
= set(os
.path
.abspath(p
) for p
in value
.split(','))
1010 if option
.dest
== 'tags':
1011 value
= [x
.strip() for x
in value
.split(',')]
1013 if option
.dest
in path_options
:
1014 setattr(parser
.values
, option
.dest
, os
.path
.abspath(value
))
1016 setattr(parser
.values
, option
.dest
, value
)
1020 parser
= OptionParser()
1021 parser
.add_option('-c', action
='callback', callback
=options_cb
,
1022 type='string', dest
='runfiles', metavar
='runfiles',
1023 help='Specify tests to run via config files.')
1024 parser
.add_option('-d', action
='store_true', default
=False, dest
='dryrun',
1025 help='Dry run. Print tests, but take no other action.')
1026 parser
.add_option('-l', action
='callback', callback
=options_cb
,
1027 default
=None, dest
='logfile', metavar
='logfile',
1029 help='Read logfile and re-run tests which failed.')
1030 parser
.add_option('-g', action
='store_true', default
=False,
1031 dest
='do_groups', help='Make directories TestGroups.')
1032 parser
.add_option('-o', action
='callback', callback
=options_cb
,
1033 default
=BASEDIR
, dest
='outputdir', type='string',
1034 metavar
='outputdir', help='Specify an output directory.')
1035 parser
.add_option('-i', action
='callback', callback
=options_cb
,
1036 default
=TESTDIR
, dest
='testdir', type='string',
1037 metavar
='testdir', help='Specify a test directory.')
1038 parser
.add_option('-p', action
='callback', callback
=options_cb
,
1039 default
='', dest
='pre', metavar
='script',
1040 type='string', help='Specify a pre script.')
1041 parser
.add_option('-P', action
='callback', callback
=options_cb
,
1042 default
='', dest
='post', metavar
='script',
1043 type='string', help='Specify a post script.')
1044 parser
.add_option('-q', action
='store_true', default
=False, dest
='quiet',
1045 help='Silence on the console during a test run.')
1046 parser
.add_option('-s', action
='callback', callback
=options_cb
,
1047 default
='', dest
='failsafe', metavar
='script',
1048 type='string', help='Specify a failsafe script.')
1049 parser
.add_option('-S', action
='callback', callback
=options_cb
,
1050 default
='', dest
='failsafe_user',
1051 metavar
='failsafe_user', type='string',
1052 help='Specify a user to execute the failsafe script.')
1053 parser
.add_option('-t', action
='callback', callback
=options_cb
, default
=60,
1054 dest
='timeout', metavar
='seconds', type='int',
1055 help='Timeout (in seconds) for an individual test.')
1056 parser
.add_option('-u', action
='callback', callback
=options_cb
,
1057 default
='', dest
='user', metavar
='user', type='string',
1058 help='Specify a different user name to run as.')
1059 parser
.add_option('-w', action
='callback', callback
=options_cb
,
1060 default
=None, dest
='template', metavar
='template',
1061 type='string', help='Create a new config file.')
1062 parser
.add_option('-x', action
='callback', callback
=options_cb
, default
='',
1063 dest
='pre_user', metavar
='pre_user', type='string',
1064 help='Specify a user to execute the pre script.')
1065 parser
.add_option('-X', action
='callback', callback
=options_cb
, default
='',
1066 dest
='post_user', metavar
='post_user', type='string',
1067 help='Specify a user to execute the post script.')
1068 parser
.add_option('-T', action
='callback', callback
=options_cb
, default
='',
1069 dest
='tags', metavar
='tags', type='string',
1070 help='Specify tags to execute specific test groups.')
1071 parser
.add_option('-I', action
='callback', callback
=options_cb
, default
=1,
1072 dest
='iterations', metavar
='iterations', type='int',
1073 help='Number of times to run the test run.')
1074 (options
, pathnames
) = parser
.parse_args()
1076 if options
.runfiles
and len(pathnames
):
1077 fail('Extraneous arguments.')
1079 options
.pathnames
= [os
.path
.abspath(path
) for path
in pathnames
]
1085 options
= parse_args()
1087 testrun
= TestRun(options
)
1089 if options
.runfiles
:
1090 testrun
.read(options
)
1092 find_tests(testrun
, options
)
1095 filter_tests(testrun
, options
)
1097 if options
.template
:
1098 testrun
.write(options
)
1101 testrun
.complete_outputdirs()
1102 testrun
.run(options
)
1103 exit(testrun
.summary())
1106 if __name__
== '__main__':