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