Package inca :: Module Reporter
[hide private]
[frames] | no frames]

Source Code for Module inca.Reporter

  1  import commands 
  2  import os 
  3  import os.path 
  4  import re 
  5  import signal 
  6  import socket 
  7  import string 
  8  import sys 
  9  import time 
 10   
11 -class Reporter:
12 """Reporter - Module for creating Inca reporters:: 13 14 from inca.Reporter import Reporter 15 reporter = Reporter( 16 name = 'hack.version', 17 version = 0.1, 18 description = 'A really helpful reporter description', 19 url = 'http://url.to.more.reporter.info' 20 ) 21 22 This module creates Inca reporters--objects that produce XML that follows 23 the Inca Report schema. The constructor may be called with a number of 24 reporter attributes that can later be set and queried with their 25 corresponding get/set functions (described below). For example:: 26 27 reporter = Reporter() 28 reporter.setUrl('http://url.to.more.reporter.info') 29 reporter.setVersion(0.1) 30 """ 31
32 - def __init__(self, **attributes):
33 """Class constructor that returns a new Reporter object. 34 35 The constructor may be called with any of the following named attributes 36 as parameters:: 37 38 body 39 the XML body of the report. See the Inca Report schema for format. 40 41 completed 42 boolean indicating whether or not the reporter has completed 43 generating the information it is intended to produce 44 45 description 46 a verbose description of the reporter 47 48 fail_message 49 a message describing why the reporter failed to complete its task 50 51 name 52 the name that identifies this reporter 53 54 url 55 URL to get more information about the reporter 56 57 version 58 the version of the reporter; defaults to '0' 59 """ 60 61 self.args = {} 62 self.argv = [] 63 self.body = None 64 self.completed = False 65 self.cwd = os.getcwd() 66 self.description = None 67 self.dependencies = [] 68 self.fail_message = None 69 self.log_entries = [] 70 self.log_pat = '^$' 71 self.name = os.path.basename(sys.argv[0]) 72 self.temp_paths = [] 73 self.url = None 74 self.version = '0' 75 76 self.addArg('help', 'display usage information (no|yes)', 'no', 'no|yes') 77 self.addArg( 78 'log', 'log message types included in report', '0', 79 '[012345]|debug|error|info|system|warn' 80 ) 81 self.addArg('verbose', 'verbosity level (0|1|2)', '1', '[012]') 82 self.addArg('version', 'show reporter version (no|yes)', 'no', 'no|yes') 83 self.addDependency('inca.Reporter') 84 85 for attr in attributes.keys(): 86 if hasattr(self, attr): 87 setattr(self, attr, attributes[attr]) 88 else: 89 sys.stderr.write("'" + attr + "' is an invalid attribute\n");
90
91 - def addArg(self, name, description=None, default=None, pattern=None):
92 """Adds a command line argument (invocation syntax -name=value) to the 93 reporter. If supplied, the optional description will be included in the 94 reporter help XML and display. If supplied, default indicates that the 95 argument is optional; the argValue method will return default if the 96 command line does not include a value for the argument. The optional 97 pattern specifies a pattern for recognizing valid argument values; the 98 default is '.*', which means that any text is acceptable for the 99 argument value. 100 """ 101 if pattern == None: 102 pattern = '.*' 103 self.args[name] = { 104 'description' : description, 'default' : default, 'pat' : pattern 105 }
106
107 - def addDependency(self, *dependencies):
108 """Add one or more dependencies to the list of modules on which this 109 reporter depends. Dependencies are reported as part of reporter help 110 output to assist reporter repository tools in their retrievals. NOTE: 111 dependencies on the standard Inca reporter library modules are added by 112 the modules themselves, so a reporter only needs to invoke this method 113 to report external dependencies. The Inca Reporter Instance Manager 114 presently only supports dependencies on Inca repository packages. 115 """ 116 self.dependencies.extend(dependencies)
117
118 - def argValue(self, name, position=None):
119 """Called after the processArgv method, this returns the value of the 120 position'th instance (starting with 1) of the name command-line 121 argument. Returns the value of the last instance if position is None. 122 Returns None if name is not a recognized argument. Returns the default 123 value for name if it has one and name is included fewer than position 124 times on the command line. 125 """ 126 if not self.args.has_key(name): 127 sys.stderr.write( 128 "'" + name + "' is not a valid command line argument name" 129 ); 130 return None 131 argv = self.argv[:] 132 if position == None: 133 argv.reverse() 134 position = 1 135 for arg in argv: 136 found = re.match(name + '=(.*)', arg) 137 if found: 138 position -= 1 139 if position < 1: 140 return found.group(1) 141 return self.args[name]['default']
142
143 - def argValues(self, name):
144 """Called after the processArgv method, this returns an array of all values 145 specified for the name command-line argument. Returns None if name is 146 not a recognized argument. Returns a single-element array containing 147 the default value for name if it has one and name does not appear on the 148 command line. 149 """ 150 if not self.args.has_key(name): 151 sys.stderr.write( 152 "'" + name + "' is not a valid command line argument name" 153 ); 154 return None 155 default = self.args[name]['default'] 156 result = [] 157 for arg in self.argv: 158 found = re.match(name + '=(.*)', arg) 159 if found: 160 result.append(found.group(1)) 161 if len(result) == 0 and default != None: 162 result.append(default) 163 return result
164
165 - def compiledProgramOutput(self, **params):
166 """A convenience; compiles and runs a program, removes the source and exec 167 files, and returns the program's combined stderr/out output. See 168 compiledProgramStatusOutput for a list of recognized params. 169 """ 170 (status, output) = self.compiledProgramStatusOutput(**params) 171 return output
172
173 - def compiledProgramStatusOutput(self, **params):
174 """A convenience; compiles and runs a program, removes the source and exec 175 files, and returns a tuple that contains the program's exit code and 176 its combined stderr/out output. Recognized params:: 177 178 code 179 the code to compile; required 180 181 compiler 182 the compiler to invoke; defaults to cc 183 184 language 185 source file language--one of 'c', 'c++', 'fortran', or 'java'; 186 defaults to 'c'. 187 188 out_switch 189 the switch to use to specify the compiler output file; default '-o ' 190 191 switches 192 additional switches to pass to the compiler; defaults to '' 193 194 timeout 195 max seconds compilation/execution may take; returns a non-zero exit 196 status and any partial program output on time-out 197 """ 198 code = params['code'] 199 compiler = 'cc' 200 if params.has_key('compiler'): 201 compiler = params['compiler'] 202 lang = 'c' 203 if params.has_key('language'): 204 lang = params['language'] 205 extension = 'c' 206 if lang == 'c++': 207 extension = 'C' 208 elif lang == 'fortran': 209 extension = 'f' 210 elif lang == 'java': 211 extension = 'java' 212 prefix = 'src' + str(os.getpid()) 213 if lang == 'java': 214 code = re.sub(r'class\s+\w+', 'class ' + prefix, code) 215 if os.environ['CLASSPATH'] != None: 216 os.environ['CLASSPATH'] += ':' 217 os.environ['CLASSPATH'] += '.' 218 path = prefix + '.' + extension 219 output = open(path, 'w') 220 if not output: 221 return None 222 output.write(code + '\n') 223 output.close() 224 out = '-o ' 225 if params.has_key('out_switch'): 226 out = params['out_switch'] 227 switches = '' 228 if params.has_key('switches'): 229 switches = params['switches'] 230 timeout = None 231 if params.has_key('timeout'): 232 timeout = params['timeout'] 233 cmd = None 234 if lang == 'java': 235 cmd = '('+compiler+' '+path+' '+switches+' && java '+prefix+')' 236 else: 237 cmd='('+compiler+' '+path+' '+out+prefix+' '+switches+' && ./'+prefix+')' 238 oldLd = None 239 if os.environ.has_key('LD_LIBRARY_PATH'): 240 oldLd = os.environ['LD_LIBRARY_PATH'] 241 if re.search(r'-L\s*\S+', switches): 242 paths = re.findall(r'-L\s*(\S+)', switches) 243 os.environ['LD_LIBRARY_PATH'] = string.join(paths, ':') 244 if oldLd != None: 245 os.environ['LD_LIBRARY_PATH'] += ':' + oldLd 246 (status, output) = self.loggedCommandStatusOutput(cmd, timeout) 247 if oldLd != None: 248 os.environ['LD_LIBRARY_PATH'] = oldLd 249 elif os.environ.has_key('LD_LIBRARY_PATH'): 250 del os.environ['LD_LIBRARY_PATH'] 251 self.loggedCommandOutput('/bin/rm -f ' + prefix + '*') 252 return (status, output)
253
254 - def failPrintAndExit(self, msg):
255 """A convenience; calls setResult(0, msg) and printReport() before exiting 256 the reporter. 257 """ 258 self.setResult(0, msg) 259 self.printReport() 260 sys.exit(0)
261
262 - def getBody(self):
263 """Returns the body of the report.""" 264 return self.body
265
266 - def getCompleted(self):
267 """Returns the completion indicator of the reporter.""" 268 return self.completed
269
270 - def getCwd(self):
271 """Returns the initial working directory of the reporter.""" 272 return self.cwd
273
274 - def getDescription(self):
275 """Returns the initial working directory of the reporter.""" 276 return self.description
277
278 - def getFailMessage(self):
279 """Returns the failure message of the reporter.""" 280 return self.fail_message
281
282 - def getName(self):
283 """Returns the name that identifies this reporter.""" 284 return self.name
285
286 - def getUrl(self):
287 """Returns the url which describes the reporter in more detail.""" 288 return self.url
289
290 - def getVersion(self):
291 """Returns the version of the reporter.""" 292 return self.version
293
294 - def log(self, type, *msgs):
295 """Appends each element of msgs to the list of type log messages stored 296 in the reporter. type must be one of 'debug', 'error', 'info', 'system', 297 or 'warn'.""" 298 if not re.search(self.log_pat, type): 299 return 300 for msg in msgs: 301 if self.argValue('verbose') == '0': 302 sys.stderr.write(type + ': ' + msg + '\n') 303 self.log_entries.append({ 304 'type' : type, 'time' : time.time(), 'msg' : msg 305 })
306
307 - def loggedCommandOutput(self, cmd, timeout=None):
308 """A convenience; appends cmd to the 'system'-type log messages stored in 309 the reporter, then runs cmd and returns its combined stderr/stdout. 310 If timeout is specified and the command doesn't complete within timeout 311 seconds, aborts the execution of cmd and returns any partial output. 312 """ 313 (status, output) = self.loggedCommandStatusOutput(cmd, timeout) 314 return output
315
316 - def loggedCommandStatusOutput(self, cmd, timeout=None):
317 """A convenience; appends cmd to the 'system'-type log messages stored in 318 the reporter, then runs cmd and returns a tuple that contains its exit 319 code and combined stderr/stdout. If timeout is specified and the 320 command doesn't complete within timeout seconds, aborts the execution 321 of cmd and returns a non-zero exit code and any partial output. 322 """ 323 self.log('system', cmd) 324 if timeout == None: 325 (status, output) = commands.getstatusoutput(cmd) 326 return (status, output + "\n") 327 # fork a child to run the command, sending stderr/out through a pipe. Set 328 # the pgrp of the child so that we can kill it and any processes it spawns. 329 (readfd, writefd) = os.pipe(); 330 childPid = os.fork() 331 if childPid == 0: 332 os.close(readfd) 333 os.dup2(writefd, 1) 334 os.dup2(writefd, 2) 335 os.setpgrp() 336 os.execl('/bin/sh', '/bin/sh', '-c', cmd) 337 os.exit(1) 338 os.close(writefd) 339 readFile = os.fdopen(readfd, 'r') 340 timedOut = False 341 # Install an alarm handler to interrupt reading the pipe/raise an exception. 342 oldHandler = signal.signal(signal.SIGALRM, self._timeoutException) 343 output = ''; 344 signal.alarm(int(timeout)) 345 try: 346 line = readFile.readline() 347 while line: 348 output += line 349 line = readFile.readline() 350 except SystemExit: 351 timedOut = True 352 signal.alarm(0) 353 if timedOut: 354 os.killpg(childPid, 9) 355 (childPid, status) = os.waitpid(childPid, 0) 356 status = os.WEXITSTATUS(status) 357 if timedOut: 358 status = 1 359 signal.signal(signal.SIGALRM, oldHandler) 360 readFile.close() 361 return (status, output)
362
363 - def printReport(self, verbose=None):
364 """A convenience; prints report(verbose) to stdout.""" 365 print self.report(verbose) + "\n"
366
367 - def processArgv(self, argv):
368 """Processes argv which is a list of command-line arguments of the form 369 -name=value 370 371 The following options are predefined:: 372 373 help 374 yes 375 Prints help information describing the reporter inputs, then 376 forces the reporter to exit. If the verbose level is 0, the 377 output will be text; otherwise, it will be Inca Report XML. 378 no (default) 379 Normal reporter execution. 380 381 log 382 0 (default) 383 log no messages 384 1 385 log error messages 386 2 387 log error and warning messages 388 3 389 log error, warning, and system messages 390 4 391 log error, warning, system, and info messages 392 5 393 log error, warning, system, info, and debug messages 394 debug 395 log only debug messages 396 error 397 log only error messages 398 info 399 log only info messages 400 system 401 log only system messages 402 warn 403 log only warning messages 404 405 verbose 406 0 407 print will only produce "completed" or "failed". 408 1 (default) 409 print will produce Inca Report XML. 410 2 411 print will produce Inca Report XML that includes help information. 412 413 version 414 yes 415 Prints the reporter version number and exits. 416 no (default) 417 Normal reporter execution. 418 """ 419 if len(argv) == 1: 420 # we have a single argument; check to see if the input is URL-style query 421 # string, e.g., -file=test.pl&help=no&verbose=1 422 argv = argv[0].split('&') 423 elif len(argv) == 0 and os.environ.has_key('QUERY_STRING'): 424 # maybe we're running as a CGI script 425 argv = os.environ['QUERY_STRING'].split('&') 426 427 argValues = [] 428 badArg = None 429 missing = [] 430 patterns = [] 431 432 for arg in argv: 433 pieces = arg.split('=') 434 name = re.sub('^--?', '', pieces[0]) 435 if len(pieces) == 1: 436 value = 'yes' 437 else: 438 value = pieces[1] 439 if not self.args.has_key(name): 440 badArg = "unknown argument '" + name + "'" 441 elif not re.search(self.args[name]['pat'], value): 442 badArg = "'" + value + "' is not a valid value for -" + name 443 argValues.append(name + '=' + value) 444 self.argv = argValues 445 if badArg != None: 446 self.failPrintAndExit(badArg) 447 448 if self.argValue('help') != 'no': 449 if self.argValue('verbose') == '0': 450 description = self.getDescription() 451 version = self.getVersion() 452 if version == None: 453 version = "No version" 454 url = self.getUrl() 455 if url == None: 456 url = "No URL" 457 text = '' 458 usage = os.path.basename(sys.argv[0]) 459 args = self.args.keys() 460 args.sort() 461 for arg in args: 462 argDefault = self.args[arg]['default'] 463 argDescription = self.args[arg]['description'] 464 text += ' -' + arg + '\n' 465 if argDescription != None: 466 text += '\t' + argDescription + '\n' 467 usage += ' -' + arg 468 if argDefault != None: 469 usage += '=' + str(argDefault) 470 print "NAME:\n " + self.getName() + "\n" + \ 471 "VERSION:\n " + str(version) + "\n" + \ 472 "URL:\n " + url + "\n" + \ 473 "SYNOPSIS:\n " + usage + "\n" + text + "\n" 474 else: 475 print self._reportXml(self._helpXml()) + "\n" 476 sys.exit(0) 477 if self.argValue('version') != 'no': 478 print self.getName() + ' ' + str(self.getVersion()) + "\n" 479 sys.exit(0) 480 481 for arg in self.args.keys(): 482 if self.argValue(arg) == None: 483 missing.append(arg) 484 if len(missing) == 1: 485 self.failPrintAndExit("Missing required argument '" + missing[0] + "'"); 486 elif len(missing) > 0: 487 self.failPrintAndExit \ 488 ("Missing required arguments '" + string.join(missing, "', '") + "'"); 489 490 for arg in self.argValues('log'): 491 if re.match('[012345]$', arg): 492 allTypes = ['error', 'warn', 'system', 'info', 'debug'] 493 arg = string.join(allTypes[0:int(arg)], '|') 494 patterns.append(arg) 495 if len(patterns) > 0: 496 self.log_pat = '^(' + string.join(patterns, '|') + ')$'
497
498 - def report(self, verbose=None):
499 """Returns report text or XML, depending on the value (0, 1, 2) of verbose. 500 Uses the value of the -verbose switch if verbose is None. 501 """ 502 completed = self.getCompleted() 503 msg = self.getFailMessage() 504 if verbose == None: 505 verbose = self.argValue('verbose') 506 if verbose == '0': 507 if completed: 508 result = 'completed' 509 else: 510 result = 'failed' 511 if msg != None: 512 result += ': ' + msg 513 else: 514 if completed and self.getBody() == None: 515 self.setBody(self.reportBody()) 516 if msg != None: 517 messageXml = self.xmlElement('errorMessage', 1, msg) 518 else: 519 messageXml = None 520 if completed: 521 completed = 'true' 522 else: 523 completed = 'false' 524 completedXml = self.xmlElement('completed', 1, completed) 525 if verbose == '2': 526 helpXml = self._helpXml() 527 else: 528 helpXml = None 529 result = self._reportXml( 530 self.xmlElement('body', 0, self.getBody()), 531 self.xmlElement('exitStatus', 0, completedXml, messageXml), 532 helpXml 533 ) 534 return result
535
536 - def reportBody(self):
537 """Constructs and returns the XML contents of the report body. Child 538 classes should override the default implementation, which returns None. 539 """ 540 return None
541
542 - def setBody(self, body):
543 """Sets the body of the report to body.""" 544 self.body = body
545
546 - def setCompleted(self, completed):
547 """Sets the completion indicator of the reporter to completed.""" 548 self.completed = completed
549
550 - def setCwd(self, cwd):
551 """Sets the initial working directory of the reporter to cwd.""" 552 self.cwd = cwd
553
554 - def setDescription(self, description):
555 """Sets the description of the reporter to description.""" 556 self.description = description
557
558 - def setFailMessage(self, msg):
559 """Sets the failure message of the reporter to msg.""" 560 self.fail_message = msg
561
562 - def setName(self, name):
563 """Sets the name that identifies this reporter to name.""" 564 self.name = name
565
566 - def setResult(self, completed, msg=None):
567 """A convenience; calls setCompleted(completed) and setFailMessage(msg).""" 568 self.setCompleted(completed) 569 self.setFailMessage(msg)
570
571 - def setUrl(self, url):
572 """Sets the url for the reporter to url.""" 573 self.url = url
574
575 - def setVersion(self, version):
576 """Sets the version of the reporter to version. Recognizes and parses CVS 577 revision strings. 578 """ 579 if version == None: 580 return 581 found = re.search('Revision: (.*) ', version) 582 if found: 583 self.version = re.group(1) 584 else: 585 self.version = version
586
587 - def tempFile(self, *paths):
588 """A convenience. Adds each element of paths to a list of temporary files 589 that will be deleted automatically when the reporter is destroyed. 590 """ 591 self.temp_paths.extend(paths)
592
593 - def xmlElement(self, name, escape, *contents):
594 """Returns the XML element name surrounding contents. escape should be 595 true only for leaf elements; in this case, each special XML character 596 (<>&) in contents is replaced by the equivalent XML entity. 597 """ 598 innards = '' 599 for content in contents: 600 if content == None: 601 continue 602 content = str(content) 603 if escape: 604 content = re.sub('&', '&amp;', content) 605 content = re.sub('<', '&lt;', content) 606 content = re.sub('>', '&gt;', content) 607 content = re.sub('^<', '\n<', content, 1) 608 innards += content 609 innards = re.sub('(?m)^( *)<', r' \1<', innards) 610 innards = re.sub('>$', '>\n', innards, 1) 611 return '<' + name + '>' + innards + '</' + name + '>'
612
613 - def __del__(self):
614 """Class destructor.""" 615 if len(self.temp_paths) > 0: 616 commands.getoutput( 617 '/bin/rm -fr ' + string.join(self.temp_paths, ' ') 618 )
619
620 - def _helpXml(self):
621 """Returns help information formatted as the body of an Inca report.""" 622 argsAndDepsXml = [] 623 args = self.args.keys() 624 args.sort() 625 for arg in args: 626 info = self.args[arg] 627 defaultXml = info['default'] 628 if defaultXml != None: 629 defaultXml = self.xmlElement('default', 1, defaultXml) 630 description = info['description'] 631 if description == None: 632 description = '' 633 argsAndDepsXml.append(self.xmlElement('argDescription', 0, 634 self.xmlElement('ID', 1, arg), 635 self.xmlElement('accepted', 1, info['pat']), 636 self.xmlElement('description', 1, description), 637 defaultXml 638 )) 639 for dep in self.dependencies: 640 argsAndDepsXml.append( 641 self.xmlElement('dependency', 0, self.xmlElement('ID', 1, dep)) 642 ) 643 return self.xmlElement('help', 0, 644 self.xmlElement('ID', 1, 'help'), 645 self.xmlElement('name', 1, self.getName()), 646 self.xmlElement('version', 1, str(self.getVersion())), 647 self.xmlElement('description', 1, self.getDescription()), 648 self.xmlElement('url', 1, self.getUrl()), \ 649 *argsAndDepsXml 650 )
651
652 - def _iso8601Time(self, when):
653 """Returns the UTC time for the time() return value when in ISO 8601 654 format: CCMM-MM-DDTHH:MM:SSZ 655 """ 656 t = time.gmtime(when) 657 return '%04d-%02d-%02dT%02d:%02d:%02dZ' % \ 658 (t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec)
659
660 - def _reportXml(self, *contents):
661 """Returns XML report beginning with the header and input sections plus any 662 contents specified in the arguments. 663 """ 664 argXmls = [] 665 logXmls = [] 666 hostname = socket.gethostname() 667 if hostname.find('.') < 0: 668 hostname = socket.getfqdn() 669 670 tags = [ 671 self.xmlElement('gmt', 1, self._iso8601Time(time.time())), 672 self.xmlElement('hostname', 1, hostname), 673 self.xmlElement('name', 1, self.getName()), 674 self.xmlElement('version', 1, str(self.getVersion())), 675 self.xmlElement('workingDir', 1, self.getCwd()), 676 self.xmlElement('reporterPath', 1, sys.argv[0]) 677 ] 678 args = self.args.keys() 679 args.sort() 680 for arg in args: 681 for value in self.argValues(arg): 682 argXmls.append \ 683 (self.xmlElement('arg', 0, self.xmlElement('name', 1, arg), \ 684 self.xmlElement('value', 1, value))) 685 tags.append(self.xmlElement('args', 0, *argXmls)) 686 for entry in self.log_entries: 687 logXmls.append(self.xmlElement(entry['type'], 0, 688 self.xmlElement('gmt', 1, self._iso8601Time(entry['time'])), 689 self.xmlElement('message', 1, entry['msg']) 690 )) 691 if len(logXmls) > 0: 692 tags.append(self.xmlElement('log', 0, *logXmls)) 693 tags.extend(contents) 694 result = self.xmlElement('rep:report', 0, *tags) 695 result = re.sub('<rep:report', "<rep:report xmlns:rep='http://inca.sdsc.edu/dataModel/report_2.1'", result, 1) 696 return "<?xml version='1.0'?>\n" + result
697
698 - def _timeoutException(*args):
699 """SIGALRM handler that throws an exception.""" 700 raise SystemExit(1)
701