Print this page
9600 LDT still not happy under KPTI
Split |
Close |
Expand all |
Collapse all |
--- old/usr/src/test/test-runner/cmd/run
+++ new/usr/src/test/test-runner/cmd/run
1 1 #!@PYTHON@
2 2
3 3 #
4 4 # This file and its contents are supplied under the terms of the
5 5 # Common Development and Distribution License ("CDDL"), version 1.0.
6 6 # You may only use this file in accordance with the terms of version
↓ open down ↓ |
6 lines elided |
↑ open up ↑ |
7 7 # 1.0 of the CDDL.
8 8 #
9 9 # A full copy of the text of the CDDL should have accompanied this
10 10 # source. A copy of the CDDL is also available via the Internet at
11 11 # http://www.illumos.org/license/CDDL.
12 12 #
13 13
14 14 #
15 15 # Copyright (c) 2012, 2016 by Delphix. All rights reserved.
16 16 # Copyright (c) 2017, Chris Fraire <cfraire@me.com>.
17 +# Copyright 2018 Joyent, Inc.
17 18 #
18 19
19 20 import ConfigParser
20 21 import os
21 22 import logging
23 +import platform
22 24 from logging.handlers import WatchedFileHandler
23 25 from datetime import datetime
24 26 from optparse import OptionParser
25 27 from pwd import getpwnam
26 28 from pwd import getpwuid
27 29 from select import select
28 30 from subprocess import PIPE
29 31 from subprocess import Popen
30 32 from sys import argv
31 33 from sys import maxint
32 34 from threading import Timer
33 35 from time import time
34 36
35 37 BASEDIR = '/var/tmp/test_results'
36 38 KILL = '/usr/bin/kill'
37 39 TRUE = '/usr/bin/true'
38 40 SUDO = '/usr/bin/sudo'
39 41
40 42 # Custom class to reopen the log file in case it is forcibly closed by a test.
41 43 class WatchedFileHandlerClosed(WatchedFileHandler):
42 44 """Watch files, including closed files.
43 45 Similar to (and inherits from) logging.handler.WatchedFileHandler,
44 46 except that IOErrors are handled by reopening the stream and retrying.
45 47 This will be retried up to a configurable number of times before
46 48 giving up, default 5.
47 49 """
48 50
49 51 def __init__(self, filename, mode='a', encoding=None, delay=0, max_tries=5):
50 52 self.max_tries = max_tries
51 53 self.tries = 0
52 54 WatchedFileHandler.__init__(self, filename, mode, encoding, delay)
53 55
54 56 def emit(self, record):
55 57 while True:
56 58 try:
57 59 WatchedFileHandler.emit(self, record)
58 60 self.tries = 0
59 61 return
60 62 except IOError as err:
61 63 if self.tries == self.max_tries:
62 64 raise
63 65 self.stream.close()
64 66 self.stream = self._open()
65 67 self.tries += 1
66 68
67 69 class Result(object):
68 70 total = 0
69 71 runresults = {'PASS': 0, 'FAIL': 0, 'SKIP': 0, 'KILLED': 0}
70 72
71 73 def __init__(self):
72 74 self.starttime = None
73 75 self.returncode = None
74 76 self.runtime = ''
75 77 self.stdout = []
76 78 self.stderr = []
77 79 self.result = ''
78 80
79 81 def done(self, proc, killed):
80 82 """
81 83 Finalize the results of this Cmd.
82 84 """
83 85 Result.total += 1
84 86 m, s = divmod(time() - self.starttime, 60)
85 87 self.runtime = '%02d:%02d' % (m, s)
86 88 self.returncode = proc.returncode
87 89 if killed:
88 90 self.result = 'KILLED'
89 91 Result.runresults['KILLED'] += 1
90 92 elif self.returncode is 0:
91 93 self.result = 'PASS'
92 94 Result.runresults['PASS'] += 1
93 95 elif self.returncode is not 0:
94 96 self.result = 'FAIL'
95 97 Result.runresults['FAIL'] += 1
96 98
97 99
98 100 class Output(object):
99 101 """
100 102 This class is a slightly modified version of the 'Stream' class found
101 103 here: http://goo.gl/aSGfv
102 104 """
103 105 def __init__(self, stream):
104 106 self.stream = stream
105 107 self._buf = ''
106 108 self.lines = []
107 109
108 110 def fileno(self):
109 111 return self.stream.fileno()
110 112
111 113 def read(self, drain=0):
112 114 """
113 115 Read from the file descriptor. If 'drain' set, read until EOF.
114 116 """
115 117 while self._read() is not None:
116 118 if not drain:
117 119 break
118 120
119 121 def _read(self):
120 122 """
121 123 Read up to 4k of data from this output stream. Collect the output
122 124 up to the last newline, and append it to any leftover data from a
123 125 previous call. The lines are stored as a (timestamp, data) tuple
124 126 for easy sorting/merging later.
125 127 """
126 128 fd = self.fileno()
127 129 buf = os.read(fd, 4096)
128 130 if not buf:
129 131 return None
130 132 if '\n' not in buf:
131 133 self._buf += buf
132 134 return []
133 135
134 136 buf = self._buf + buf
135 137 tmp, rest = buf.rsplit('\n', 1)
136 138 self._buf = rest
137 139 now = datetime.now()
138 140 rows = tmp.split('\n')
139 141 self.lines += [(now, r) for r in rows]
140 142
141 143
142 144 class Cmd(object):
143 145 verified_users = []
144 146
145 147 def __init__(self, pathname, outputdir=None, timeout=None, user=None):
146 148 self.pathname = pathname
147 149 self.outputdir = outputdir or 'BASEDIR'
148 150 self.timeout = timeout
149 151 self.user = user or ''
150 152 self.killed = False
151 153 self.result = Result()
152 154
153 155 if self.timeout is None:
154 156 self.timeout = 60
155 157
156 158 def __str__(self):
157 159 return "Pathname: %s\nOutputdir: %s\nTimeout: %s\nUser: %s\n" % \
158 160 (self.pathname, self.outputdir, self.timeout, self.user)
159 161
160 162 def kill_cmd(self, proc):
161 163 """
162 164 Kill a running command due to timeout, or ^C from the keyboard. If
163 165 sudo is required, this user was verified previously.
164 166 """
165 167 self.killed = True
166 168 do_sudo = len(self.user) != 0
167 169 signal = '-TERM'
168 170
169 171 cmd = [SUDO, KILL, signal, str(proc.pid)]
170 172 if not do_sudo:
171 173 del cmd[0]
172 174
173 175 try:
174 176 kp = Popen(cmd)
175 177 kp.wait()
176 178 except:
177 179 pass
178 180
179 181 def update_cmd_privs(self, cmd, user):
180 182 """
181 183 If a user has been specified to run this Cmd and we're not already
182 184 running as that user, prepend the appropriate sudo command to run
183 185 as that user.
184 186 """
185 187 me = getpwuid(os.getuid())
186 188
187 189 if not user or user is me:
188 190 return cmd
189 191
190 192 ret = '%s -E -u %s %s' % (SUDO, user, cmd)
191 193 return ret.split(' ')
192 194
193 195 def collect_output(self, proc):
194 196 """
195 197 Read from stdout/stderr as data becomes available, until the
196 198 process is no longer running. Return the lines from the stdout and
197 199 stderr Output objects.
198 200 """
199 201 out = Output(proc.stdout)
200 202 err = Output(proc.stderr)
201 203 res = []
202 204 while proc.returncode is None:
203 205 proc.poll()
204 206 res = select([out, err], [], [], .1)
205 207 for fd in res[0]:
206 208 fd.read()
207 209 for fd in res[0]:
208 210 fd.read(drain=1)
209 211
210 212 return out.lines, err.lines
211 213
212 214 def run(self, options):
213 215 """
214 216 This is the main function that runs each individual test.
215 217 Determine whether or not the command requires sudo, and modify it
216 218 if needed. Run the command, and update the result object.
217 219 """
218 220 if options.dryrun is True:
219 221 print self
220 222 return
221 223
222 224 privcmd = self.update_cmd_privs(self.pathname, self.user)
223 225 try:
224 226 old = os.umask(0)
225 227 if not os.path.isdir(self.outputdir):
226 228 os.makedirs(self.outputdir, mode=0777)
227 229 os.umask(old)
228 230 except OSError, e:
229 231 fail('%s' % e)
230 232
231 233 try:
232 234 self.result.starttime = time()
233 235 proc = Popen(privcmd, stdout=PIPE, stderr=PIPE, stdin=PIPE)
234 236 proc.stdin.close()
235 237
236 238 # Allow a special timeout value of 0 to mean infinity
237 239 if int(self.timeout) == 0:
238 240 self.timeout = maxint
239 241 t = Timer(int(self.timeout), self.kill_cmd, [proc])
240 242 t.start()
241 243 self.result.stdout, self.result.stderr = self.collect_output(proc)
242 244 except KeyboardInterrupt:
243 245 self.kill_cmd(proc)
244 246 fail('\nRun terminated at user request.')
245 247 finally:
246 248 t.cancel()
247 249
248 250 self.result.done(proc, self.killed)
249 251
250 252 def skip(self):
251 253 """
252 254 Initialize enough of the test result that we can log a skipped
253 255 command.
254 256 """
255 257 Result.total += 1
256 258 Result.runresults['SKIP'] += 1
257 259 self.result.stdout = self.result.stderr = []
258 260 self.result.starttime = time()
259 261 m, s = divmod(time() - self.result.starttime, 60)
260 262 self.result.runtime = '%02d:%02d' % (m, s)
261 263 self.result.result = 'SKIP'
262 264
263 265 def log(self, logger, options):
264 266 """
265 267 This function is responsible for writing all output. This includes
266 268 the console output, the logfile of all results (with timestamped
267 269 merged stdout and stderr), and for each test, the unmodified
268 270 stdout/stderr/merged in it's own file.
269 271 """
270 272 if logger is None:
271 273 return
272 274
273 275 logname = getpwuid(os.getuid()).pw_name
274 276 user = ' (run as %s)' % (self.user if len(self.user) else logname)
275 277 msga = 'Test: %s%s ' % (self.pathname, user)
276 278 msgb = '[%s] [%s]' % (self.result.runtime, self.result.result)
277 279 pad = ' ' * (80 - (len(msga) + len(msgb)))
278 280
279 281 # If -q is specified, only print a line for tests that didn't pass.
280 282 # This means passing tests need to be logged as DEBUG, or the one
281 283 # line summary will only be printed in the logfile for failures.
282 284 if not options.quiet:
283 285 logger.info('%s%s%s' % (msga, pad, msgb))
284 286 elif self.result.result is not 'PASS':
285 287 logger.info('%s%s%s' % (msga, pad, msgb))
286 288 else:
287 289 logger.debug('%s%s%s' % (msga, pad, msgb))
288 290
289 291 lines = sorted(self.result.stdout + self.result.stderr,
290 292 cmp=lambda x, y: cmp(x[0], y[0]))
291 293
292 294 for dt, line in lines:
293 295 logger.debug('%s %s' % (dt.strftime("%H:%M:%S.%f ")[:11], line))
294 296
295 297 if len(self.result.stdout):
296 298 with open(os.path.join(self.outputdir, 'stdout'), 'w') as out:
297 299 for _, line in self.result.stdout:
298 300 os.write(out.fileno(), '%s\n' % line)
299 301 if len(self.result.stderr):
300 302 with open(os.path.join(self.outputdir, 'stderr'), 'w') as err:
301 303 for _, line in self.result.stderr:
302 304 os.write(err.fileno(), '%s\n' % line)
303 305 if len(self.result.stdout) and len(self.result.stderr):
304 306 with open(os.path.join(self.outputdir, 'merged'), 'w') as merged:
305 307 for _, line in lines:
306 308 os.write(merged.fileno(), '%s\n' % line)
307 309
308 310
309 311 class Test(Cmd):
310 312 props = ['outputdir', 'timeout', 'user', 'pre', 'pre_user', 'post',
311 313 'post_user']
312 314
313 315 def __init__(self, pathname, outputdir=None, timeout=None, user=None,
314 316 pre=None, pre_user=None, post=None, post_user=None):
315 317 super(Test, self).__init__(pathname, outputdir, timeout, user)
316 318 self.pre = pre or ''
317 319 self.pre_user = pre_user or ''
318 320 self.post = post or ''
319 321 self.post_user = post_user or ''
320 322
321 323 def __str__(self):
322 324 post_user = pre_user = ''
323 325 if len(self.pre_user):
324 326 pre_user = ' (as %s)' % (self.pre_user)
325 327 if len(self.post_user):
326 328 post_user = ' (as %s)' % (self.post_user)
327 329 return "Pathname: %s\nOutputdir: %s\nTimeout: %d\nPre: %s%s\nPost: " \
328 330 "%s%s\nUser: %s\n" % \
329 331 (self.pathname, self.outputdir, self.timeout, self.pre,
330 332 pre_user, self.post, post_user, self.user)
331 333
332 334 def verify(self, logger):
333 335 """
334 336 Check the pre/post scripts, user and Test. Omit the Test from this
335 337 run if there are any problems.
336 338 """
337 339 files = [self.pre, self.pathname, self.post]
338 340 users = [self.pre_user, self.user, self.post_user]
339 341
340 342 for f in [f for f in files if len(f)]:
341 343 if not verify_file(f):
342 344 logger.info("Warning: Test '%s' not added to this run because"
343 345 " it failed verification." % f)
344 346 return False
345 347
346 348 for user in [user for user in users if len(user)]:
347 349 if not verify_user(user, logger):
348 350 logger.info("Not adding Test '%s' to this run." %
349 351 self.pathname)
350 352 return False
351 353
352 354 return True
353 355
354 356 def run(self, logger, options):
355 357 """
356 358 Create Cmd instances for the pre/post scripts. If the pre script
357 359 doesn't pass, skip this Test. Run the post script regardless.
358 360 """
359 361 odir = os.path.join(self.outputdir, os.path.basename(self.pre))
360 362 pretest = Cmd(self.pre, outputdir=odir, timeout=self.timeout,
361 363 user=self.pre_user)
362 364 test = Cmd(self.pathname, outputdir=self.outputdir,
363 365 timeout=self.timeout, user=self.user)
364 366 odir = os.path.join(self.outputdir, os.path.basename(self.post))
365 367 posttest = Cmd(self.post, outputdir=odir, timeout=self.timeout,
366 368 user=self.post_user)
367 369
368 370 cont = True
369 371 if len(pretest.pathname):
370 372 pretest.run(options)
371 373 cont = pretest.result.result is 'PASS'
372 374 pretest.log(logger, options)
373 375
374 376 if cont:
375 377 test.run(options)
376 378 else:
377 379 test.skip()
378 380
379 381 test.log(logger, options)
380 382
381 383 if len(posttest.pathname):
382 384 posttest.run(options)
383 385 posttest.log(logger, options)
384 386
385 387
386 388 class TestGroup(Test):
387 389 props = Test.props + ['tests']
388 390
389 391 def __init__(self, pathname, outputdir=None, timeout=None, user=None,
390 392 pre=None, pre_user=None, post=None, post_user=None,
391 393 tests=None):
392 394 super(TestGroup, self).__init__(pathname, outputdir, timeout, user,
393 395 pre, pre_user, post, post_user)
394 396 self.tests = tests or []
395 397
396 398 def __str__(self):
397 399 post_user = pre_user = ''
398 400 if len(self.pre_user):
399 401 pre_user = ' (as %s)' % (self.pre_user)
400 402 if len(self.post_user):
401 403 post_user = ' (as %s)' % (self.post_user)
402 404 return "Pathname: %s\nOutputdir: %s\nTests: %s\nTimeout: %d\n" \
403 405 "Pre: %s%s\nPost: %s%s\nUser: %s\n" % \
404 406 (self.pathname, self.outputdir, self.tests, self.timeout,
405 407 self.pre, pre_user, self.post, post_user, self.user)
406 408
407 409 def verify(self, logger):
408 410 """
409 411 Check the pre/post scripts, user and tests in this TestGroup. Omit
410 412 the TestGroup entirely, or simply delete the relevant tests in the
411 413 group, if that's all that's required.
412 414 """
413 415 # If the pre or post scripts are relative pathnames, convert to
414 416 # absolute, so they stand a chance of passing verification.
415 417 if len(self.pre) and not os.path.isabs(self.pre):
416 418 self.pre = os.path.join(self.pathname, self.pre)
417 419 if len(self.post) and not os.path.isabs(self.post):
418 420 self.post = os.path.join(self.pathname, self.post)
419 421
420 422 auxfiles = [self.pre, self.post]
421 423 users = [self.pre_user, self.user, self.post_user]
422 424
423 425 for f in [f for f in auxfiles if len(f)]:
424 426 if self.pathname != os.path.dirname(f):
425 427 logger.info("Warning: TestGroup '%s' not added to this run. "
426 428 "Auxiliary script '%s' exists in a different "
427 429 "directory." % (self.pathname, f))
428 430 return False
429 431
430 432 if not verify_file(f):
431 433 logger.info("Warning: TestGroup '%s' not added to this run. "
432 434 "Auxiliary script '%s' failed verification." %
433 435 (self.pathname, f))
434 436 return False
435 437
436 438 for user in [user for user in users if len(user)]:
437 439 if not verify_user(user, logger):
438 440 logger.info("Not adding TestGroup '%s' to this run." %
439 441 self.pathname)
440 442 return False
441 443
442 444 # If one of the tests is invalid, delete it, log it, and drive on.
443 445 self.tests[:] = [f for f in self.tests if
444 446 verify_file(os.path.join(self.pathname, f))]
445 447
446 448 return len(self.tests) is not 0
447 449
448 450 def run(self, logger, options):
449 451 """
450 452 Create Cmd instances for the pre/post scripts. If the pre script
451 453 doesn't pass, skip all the tests in this TestGroup. Run the post
452 454 script regardless.
453 455 """
454 456 odir = os.path.join(self.outputdir, os.path.basename(self.pre))
455 457 pretest = Cmd(self.pre, outputdir=odir, timeout=self.timeout,
456 458 user=self.pre_user)
457 459 odir = os.path.join(self.outputdir, os.path.basename(self.post))
458 460 posttest = Cmd(self.post, outputdir=odir, timeout=self.timeout,
459 461 user=self.post_user)
460 462
461 463 cont = True
462 464 if len(pretest.pathname):
463 465 pretest.run(options)
464 466 cont = pretest.result.result is 'PASS'
465 467 pretest.log(logger, options)
466 468
467 469 for fname in self.tests:
468 470 test = Cmd(os.path.join(self.pathname, fname),
469 471 outputdir=os.path.join(self.outputdir, fname),
470 472 timeout=self.timeout, user=self.user)
471 473 if cont:
472 474 test.run(options)
473 475 else:
474 476 test.skip()
475 477
476 478 test.log(logger, options)
477 479
478 480 if len(posttest.pathname):
479 481 posttest.run(options)
480 482 posttest.log(logger, options)
481 483
482 484
483 485 class TestRun(object):
484 486 props = ['quiet', 'outputdir']
485 487
486 488 def __init__(self, options):
487 489 self.tests = {}
488 490 self.testgroups = {}
489 491 self.starttime = time()
490 492 self.timestamp = datetime.now().strftime('%Y%m%dT%H%M%S')
491 493 self.outputdir = os.path.join(options.outputdir, self.timestamp)
492 494 self.logger = self.setup_logging(options)
493 495 self.defaults = [
494 496 ('outputdir', BASEDIR),
495 497 ('quiet', False),
496 498 ('timeout', 60),
497 499 ('user', ''),
498 500 ('pre', ''),
499 501 ('pre_user', ''),
500 502 ('post', ''),
501 503 ('post_user', '')
502 504 ]
503 505
504 506 def __str__(self):
505 507 s = 'TestRun:\n outputdir: %s\n' % self.outputdir
506 508 s += 'TESTS:\n'
507 509 for key in sorted(self.tests.keys()):
508 510 s += '%s%s' % (self.tests[key].__str__(), '\n')
509 511 s += 'TESTGROUPS:\n'
510 512 for key in sorted(self.testgroups.keys()):
511 513 s += '%s%s' % (self.testgroups[key].__str__(), '\n')
512 514 return s
513 515
514 516 def addtest(self, pathname, options):
515 517 """
516 518 Create a new Test, and apply any properties that were passed in
517 519 from the command line. If it passes verification, add it to the
518 520 TestRun.
519 521 """
520 522 test = Test(pathname)
521 523 for prop in Test.props:
522 524 setattr(test, prop, getattr(options, prop))
523 525
524 526 if test.verify(self.logger):
525 527 self.tests[pathname] = test
526 528
527 529 def addtestgroup(self, dirname, filenames, options):
528 530 """
529 531 Create a new TestGroup, and apply any properties that were passed
530 532 in from the command line. If it passes verification, add it to the
531 533 TestRun.
532 534 """
533 535 if dirname not in self.testgroups:
534 536 testgroup = TestGroup(dirname)
535 537 for prop in Test.props:
536 538 setattr(testgroup, prop, getattr(options, prop))
537 539
538 540 # Prevent pre/post scripts from running as regular tests
539 541 for f in [testgroup.pre, testgroup.post]:
540 542 if f in filenames:
541 543 del filenames[filenames.index(f)]
542 544
543 545 self.testgroups[dirname] = testgroup
544 546 self.testgroups[dirname].tests = sorted(filenames)
545 547
546 548 testgroup.verify(self.logger)
547 549
548 550 def read(self, logger, options):
549 551 """
550 552 Read in the specified runfile, and apply the TestRun properties
551 553 listed in the 'DEFAULT' section to our TestRun. Then read each
552 554 section, and apply the appropriate properties to the Test or
553 555 TestGroup. Properties from individual sections override those set
554 556 in the 'DEFAULT' section. If the Test or TestGroup passes
555 557 verification, add it to the TestRun.
556 558 """
↓ open down ↓ |
525 lines elided |
↑ open up ↑ |
557 559 config = ConfigParser.RawConfigParser()
558 560 if not len(config.read(options.runfile)):
559 561 fail("Coulnd't read config file %s" % options.runfile)
560 562
561 563 for opt in TestRun.props:
562 564 if config.has_option('DEFAULT', opt):
563 565 setattr(self, opt, config.get('DEFAULT', opt))
564 566 self.outputdir = os.path.join(self.outputdir, self.timestamp)
565 567
566 568 for section in config.sections():
569 + if ('arch' in config.options(section) and
570 + platform.machine() != config.get(section, 'arch')):
571 + continue
572 +
567 573 if 'tests' in config.options(section):
568 574 testgroup = TestGroup(section)
569 575 for prop in TestGroup.props:
570 576 for sect in ['DEFAULT', section]:
571 577 if config.has_option(sect, prop):
572 578 setattr(testgroup, prop, config.get(sect, prop))
573 579
574 580 # Repopulate tests using eval to convert the string to a list
575 581 testgroup.tests = eval(config.get(section, 'tests'))
576 582
577 583 if testgroup.verify(logger):
578 584 self.testgroups[section] = testgroup
579 585
580 586 elif 'autotests' in config.options(section):
581 587 testgroup = TestGroup(section)
582 588 for prop in TestGroup.props:
583 589 for sect in ['DEFAULT', section]:
584 590 if config.has_option(sect, prop):
585 591 setattr(testgroup, prop, config.get(sect, prop))
586 592
587 593 filenames = os.listdir(section)
588 594 # only files starting with "tst." are considered tests
589 595 filenames = [f for f in filenames if f.startswith("tst.")]
590 596 testgroup.tests = sorted(filenames)
591 597
592 598 if testgroup.verify(logger):
593 599 self.testgroups[section] = testgroup
594 600
595 601 else:
596 602 test = Test(section)
597 603 for prop in Test.props:
598 604 for sect in ['DEFAULT', section]:
599 605 if config.has_option(sect, prop):
600 606 setattr(test, prop, config.get(sect, prop))
601 607
602 608 if test.verify(logger):
603 609 self.tests[section] = test
604 610
605 611 def write(self, options):
606 612 """
607 613 Create a configuration file for editing and later use. The
608 614 'DEFAULT' section of the config file is created from the
609 615 properties that were specified on the command line. Tests are
610 616 simply added as sections that inherit everything from the
611 617 'DEFAULT' section. TestGroups are the same, except they get an
612 618 option including all the tests to run in that directory.
613 619 """
614 620
615 621 defaults = dict([(prop, getattr(options, prop)) for prop, _ in
616 622 self.defaults])
617 623 config = ConfigParser.RawConfigParser(defaults)
618 624
619 625 for test in sorted(self.tests.keys()):
620 626 config.add_section(test)
621 627
622 628 for testgroup in sorted(self.testgroups.keys()):
623 629 config.add_section(testgroup)
624 630 config.set(testgroup, 'tests', self.testgroups[testgroup].tests)
625 631
626 632 try:
627 633 with open(options.template, 'w') as f:
628 634 return config.write(f)
629 635 except IOError:
630 636 fail('Could not open \'%s\' for writing.' % options.template)
631 637
632 638 def complete_outputdirs(self):
633 639 """
634 640 Collect all the pathnames for Tests, and TestGroups. Work
635 641 backwards one pathname component at a time, to create a unique
636 642 directory name in which to deposit test output. Tests will be able
637 643 to write output files directly in the newly modified outputdir.
638 644 TestGroups will be able to create one subdirectory per test in the
639 645 outputdir, and are guaranteed uniqueness because a group can only
640 646 contain files in one directory. Pre and post tests will create a
641 647 directory rooted at the outputdir of the Test or TestGroup in
642 648 question for their output.
643 649 """
644 650 done = False
645 651 components = 0
646 652 tmp_dict = dict(self.tests.items() + self.testgroups.items())
647 653 total = len(tmp_dict)
648 654 base = self.outputdir
649 655
650 656 while not done:
651 657 l = []
652 658 components -= 1
653 659 for testfile in tmp_dict.keys():
654 660 uniq = '/'.join(testfile.split('/')[components:]).lstrip('/')
655 661 if uniq not in l:
656 662 l.append(uniq)
657 663 tmp_dict[testfile].outputdir = os.path.join(base, uniq)
658 664 else:
659 665 break
660 666 done = total == len(l)
661 667
662 668 def setup_logging(self, options):
663 669 """
664 670 Two loggers are set up here. The first is for the logfile which
665 671 will contain one line summarizing the test, including the test
666 672 name, result, and running time. This logger will also capture the
667 673 timestamped combined stdout and stderr of each run. The second
668 674 logger is optional console output, which will contain only the one
669 675 line summary. The loggers are initialized at two different levels
670 676 to facilitate segregating the output.
671 677 """
672 678 if options.dryrun is True:
673 679 return
674 680
675 681 testlogger = logging.getLogger(__name__)
676 682 testlogger.setLevel(logging.DEBUG)
677 683
678 684 if options.cmd is not 'wrconfig':
679 685 try:
680 686 old = os.umask(0)
681 687 os.makedirs(self.outputdir, mode=0777)
682 688 os.umask(old)
683 689 except OSError, e:
684 690 fail('%s' % e)
685 691 filename = os.path.join(self.outputdir, 'log')
686 692
687 693 logfile = WatchedFileHandlerClosed(filename)
688 694 logfile.setLevel(logging.DEBUG)
689 695 logfilefmt = logging.Formatter('%(message)s')
690 696 logfile.setFormatter(logfilefmt)
691 697 testlogger.addHandler(logfile)
692 698
693 699 cons = logging.StreamHandler()
694 700 cons.setLevel(logging.INFO)
695 701 consfmt = logging.Formatter('%(message)s')
696 702 cons.setFormatter(consfmt)
697 703 testlogger.addHandler(cons)
698 704
699 705 return testlogger
700 706
701 707 def run(self, options):
702 708 """
703 709 Walk through all the Tests and TestGroups, calling run().
704 710 """
705 711 if not options.dryrun:
706 712 try:
707 713 os.chdir(self.outputdir)
708 714 except OSError:
709 715 fail('Could not change to directory %s' % self.outputdir)
710 716 for test in sorted(self.tests.keys()):
711 717 self.tests[test].run(self.logger, options)
712 718 for testgroup in sorted(self.testgroups.keys()):
713 719 self.testgroups[testgroup].run(self.logger, options)
714 720
715 721 def summary(self):
716 722 if Result.total is 0:
717 723 return
718 724
719 725 print '\nResults Summary'
720 726 for key in Result.runresults.keys():
721 727 if Result.runresults[key] is not 0:
722 728 print '%s\t% 4d' % (key, Result.runresults[key])
723 729
724 730 m, s = divmod(time() - self.starttime, 60)
725 731 h, m = divmod(m, 60)
726 732 print '\nRunning Time:\t%02d:%02d:%02d' % (h, m, s)
727 733 print 'Percent passed:\t%.1f%%' % ((float(Result.runresults['PASS']) /
728 734 float(Result.total)) * 100)
729 735 print 'Log directory:\t%s' % self.outputdir
730 736
731 737
732 738 def verify_file(pathname):
733 739 """
734 740 Verify that the supplied pathname is an executable regular file.
735 741 """
736 742 if os.path.isdir(pathname) or os.path.islink(pathname):
737 743 return False
738 744
739 745 if os.path.isfile(pathname) and os.access(pathname, os.X_OK):
740 746 return True
741 747
742 748 return False
743 749
744 750
745 751 def verify_user(user, logger):
746 752 """
747 753 Verify that the specified user exists on this system, and can execute
748 754 sudo without being prompted for a password.
749 755 """
750 756 testcmd = [SUDO, '-n', '-u', user, TRUE]
751 757
752 758 if user in Cmd.verified_users:
753 759 return True
754 760
755 761 try:
756 762 _ = getpwnam(user)
757 763 except KeyError:
758 764 logger.info("Warning: user '%s' does not exist.", user)
759 765 return False
760 766
761 767 p = Popen(testcmd)
762 768 p.wait()
763 769 if p.returncode is not 0:
764 770 logger.info("Warning: user '%s' cannot use passwordless sudo.", user)
765 771 return False
766 772 else:
767 773 Cmd.verified_users.append(user)
768 774
769 775 return True
770 776
771 777
772 778 def find_tests(testrun, options):
773 779 """
774 780 For the given list of pathnames, add files as Tests. For directories,
775 781 if do_groups is True, add the directory as a TestGroup. If False,
776 782 recursively search for executable files.
777 783 """
778 784
779 785 for p in sorted(options.pathnames):
780 786 if os.path.isdir(p):
781 787 for dirname, _, filenames in os.walk(p):
782 788 if options.do_groups:
783 789 testrun.addtestgroup(dirname, filenames, options)
784 790 else:
785 791 for f in sorted(filenames):
786 792 testrun.addtest(os.path.join(dirname, f), options)
787 793 else:
788 794 testrun.addtest(p, options)
789 795
790 796
791 797 def fail(retstr, ret=1):
792 798 print '%s: %s' % (argv[0], retstr)
793 799 exit(ret)
794 800
795 801
796 802 def options_cb(option, opt_str, value, parser):
797 803 path_options = ['runfile', 'outputdir', 'template']
798 804
799 805 if option.dest is 'runfile' and '-w' in parser.rargs or \
800 806 option.dest is 'template' and '-c' in parser.rargs:
801 807 fail('-c and -w are mutually exclusive.')
802 808
803 809 if opt_str in parser.rargs:
804 810 fail('%s may only be specified once.' % opt_str)
805 811
806 812 if option.dest is 'runfile':
807 813 parser.values.cmd = 'rdconfig'
808 814 if option.dest is 'template':
809 815 parser.values.cmd = 'wrconfig'
810 816
811 817 setattr(parser.values, option.dest, value)
812 818 if option.dest in path_options:
813 819 setattr(parser.values, option.dest, os.path.abspath(value))
814 820
815 821
816 822 def parse_args():
817 823 parser = OptionParser()
818 824 parser.add_option('-c', action='callback', callback=options_cb,
819 825 type='string', dest='runfile', metavar='runfile',
820 826 help='Specify tests to run via config file.')
821 827 parser.add_option('-d', action='store_true', default=False, dest='dryrun',
822 828 help='Dry run. Print tests, but take no other action.')
823 829 parser.add_option('-g', action='store_true', default=False,
824 830 dest='do_groups', help='Make directories TestGroups.')
825 831 parser.add_option('-o', action='callback', callback=options_cb,
826 832 default=BASEDIR, dest='outputdir', type='string',
827 833 metavar='outputdir', help='Specify an output directory.')
828 834 parser.add_option('-p', action='callback', callback=options_cb,
829 835 default='', dest='pre', metavar='script',
830 836 type='string', help='Specify a pre script.')
831 837 parser.add_option('-P', action='callback', callback=options_cb,
832 838 default='', dest='post', metavar='script',
833 839 type='string', help='Specify a post script.')
834 840 parser.add_option('-q', action='store_true', default=False, dest='quiet',
835 841 help='Silence on the console during a test run.')
836 842 parser.add_option('-t', action='callback', callback=options_cb, default=60,
837 843 dest='timeout', metavar='seconds', type='int',
838 844 help='Timeout (in seconds) for an individual test.')
839 845 parser.add_option('-u', action='callback', callback=options_cb,
840 846 default='', dest='user', metavar='user', type='string',
841 847 help='Specify a different user name to run as.')
842 848 parser.add_option('-w', action='callback', callback=options_cb,
843 849 default=None, dest='template', metavar='template',
844 850 type='string', help='Create a new config file.')
845 851 parser.add_option('-x', action='callback', callback=options_cb, default='',
846 852 dest='pre_user', metavar='pre_user', type='string',
847 853 help='Specify a user to execute the pre script.')
848 854 parser.add_option('-X', action='callback', callback=options_cb, default='',
849 855 dest='post_user', metavar='post_user', type='string',
850 856 help='Specify a user to execute the post script.')
851 857 (options, pathnames) = parser.parse_args()
852 858
853 859 if not options.runfile and not options.template:
854 860 options.cmd = 'runtests'
855 861
856 862 if options.runfile and len(pathnames):
857 863 fail('Extraneous arguments.')
858 864
859 865 options.pathnames = [os.path.abspath(path) for path in pathnames]
860 866
861 867 return options
862 868
863 869
864 870 def main():
865 871 options = parse_args()
866 872 testrun = TestRun(options)
867 873
868 874 if options.cmd is 'runtests':
869 875 find_tests(testrun, options)
870 876 elif options.cmd is 'rdconfig':
871 877 testrun.read(testrun.logger, options)
872 878 elif options.cmd is 'wrconfig':
873 879 find_tests(testrun, options)
874 880 testrun.write(options)
875 881 exit(0)
876 882 else:
877 883 fail('Unknown command specified')
878 884
879 885 testrun.complete_outputdirs()
880 886 testrun.run(options)
881 887 testrun.summary()
882 888 exit(0)
883 889
884 890
885 891 if __name__ == '__main__':
886 892 main()
↓ open down ↓ |
310 lines elided |
↑ open up ↑ |
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX