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. 177 178 Recognized params: 179 180 code 181 the code to compile; required 182 183 compiler 184 the compiler to invoke; defaults to cc 185 186 language 187 source file language--one of 'c', 'c++', 'fortran', or 'java'; 188 defaults to 'c'. 189 190 out_switch 191 the switch to use to specify the compiler output file; default '-o ' 192 193 switches 194 additional switches to pass to the compiler; defaults to '' 195 196 timeout 197 max seconds compilation/execution may take; returns a non-zero exit 198 status and any partial program output on time-out 199 """ 200 code = params['code'] 201 compiler = 'cc' 202 if params.has_key('compiler'): 203 compiler = params['compiler'] 204 lang = 'c' 205 if params.has_key('language'): 206 lang = params['language'] 207 extension = 'c' 208 if lang == 'c++': 209 extension = 'C' 210 elif lang == 'fortran': 211 extension = 'f' 212 elif lang == 'java': 213 extension = 'java' 214 prefix = 'src' + str(os.getpid()) 215 if lang == 'java': 216 code = re.sub(r'class\s+\w+', 'class ' + prefix, code) 217 if os.environ['CLASSPATH'] != None: 218 os.environ['CLASSPATH'] += ':' 219 os.environ['CLASSPATH'] += '.' 220 path = prefix + '.' + extension 221 output = open(path, 'w') 222 if not output: 223 return None 224 output.write(code + '\n') 225 output.close() 226 out = '-o ' 227 if params.has_key('out_switch'): 228 out = params['out_switch'] 229 switches = '' 230 if params.has_key('switches'): 231 switches = params['switches'] 232 timeout = None 233 if params.has_key('timeout'): 234 timeout = params['timeout'] 235 cmd = None 236 if lang == 'java': 237 cmd = '('+compiler+' '+path+' '+switches+' && java '+prefix+')' 238 else: 239 cmd='('+compiler+' '+path+' '+out+prefix+' '+switches+' && ./'+prefix+')' 240 oldLd = None 241 if os.environ.has_key('LD_LIBRARY_PATH'): 242 oldLd = os.environ['LD_LIBRARY_PATH'] 243 if re.search(r'-L\s*\S+', switches): 244 paths = re.findall(r'-L\s*(\S+)', switches) 245 os.environ['LD_LIBRARY_PATH'] = string.join(paths, ':') 246 if oldLd != None: 247 os.environ['LD_LIBRARY_PATH'] += ':' + oldLd 248 (status, output) = self.loggedCommandStatusOutput(cmd, timeout) 249 if oldLd != None: 250 os.environ['LD_LIBRARY_PATH'] = oldLd 251 elif os.environ.has_key('LD_LIBRARY_PATH'): 252 del os.environ['LD_LIBRARY_PATH'] 253 self.loggedCommandOutput('/bin/rm -f ' + prefix + '*') 254 return (status, output)
255
256 - def failPrintAndExit(self, msg):
257 """A convenience; calls setResult(0, msg) and printReport() before exiting 258 the reporter. 259 """ 260 self.setResult(0, msg) 261 self.printReport() 262 sys.exit(0)
263
264 - def getBody(self):
265 """Returns the body of the report.""" 266 return self.body
267
268 - def getCompleted(self):
269 """Returns the completion indicator of the reporter.""" 270 return self.completed
271
272 - def getCwd(self):
273 """Returns the initial working directory of the reporter.""" 274 return self.cwd
275
276 - def getDescription(self):
277 """Returns the initial working directory of the reporter.""" 278 return self.description
279
280 - def getFailMessage(self):
281 """Returns the failure message of the reporter.""" 282 return self.fail_message
283
284 - def getName(self):
285 """Returns the name that identifies this reporter.""" 286 return self.name
287
288 - def getUrl(self):
289 """Returns the url which describes the reporter in more detail.""" 290 return self.url
291
292 - def getVersion(self):
293 """Returns the version of the reporter.""" 294 return self.version
295
296 - def log(self, type, *msgs):
297 """Appends each element of msgs to the list of type log messages stored 298 in the reporter. type must be one of 'debug', 'error', 'info', 'system', 299 or 'warn'.""" 300 if not re.search(self.log_pat, type): 301 return 302 for msg in msgs: 303 if self.argValue('verbose') == '0': 304 sys.stderr.write(type + ': ' + msg + '\n') 305 self.log_entries.append({ 306 'type' : type, 'time' : time.time(), 'msg' : msg 307 })
308
309 - def loggedCommandOutput(self, cmd, timeout=None):
310 """A convenience; appends cmd to the 'system'-type log messages stored in 311 the reporter, then runs cmd and returns its combined stderr/stdout. 312 If timeout is specified and the command doesn't complete within timeout 313 seconds, aborts the execution of cmd and returns any partial output. 314 """ 315 (status, output) = self.loggedCommandStatusOutput(cmd, timeout) 316 return output
317
318 - def loggedCommandStatusOutput(self, cmd, timeout=None):
319 """A convenience; appends cmd to the 'system'-type log messages stored in 320 the reporter, then runs cmd and returns a tuple that contains its exit 321 code and combined stderr/stdout. If timeout is specified and the 322 command doesn't complete within timeout seconds, aborts the execution 323 of cmd and returns a non-zero exit code and any partial output. 324 """ 325 self.log('system', cmd) 326 if timeout == None: 327 (status, output) = commands.getstatusoutput(cmd) 328 return (status, output + "\n") 329 # fork a child to run the command, sending stderr/out through a pipe. Set 330 # the pgrp of the child so that we can kill it and any processes it spawns. 331 (readfd, writefd) = os.pipe(); 332 childPid = os.fork() 333 if childPid == 0: 334 os.close(readfd) 335 os.dup2(writefd, 1) 336 os.dup2(writefd, 2) 337 os.setpgrp() 338 os.execl('/bin/sh', '/bin/sh', '-c', cmd) 339 os.exit(1) 340 os.close(writefd) 341 readFile = os.fdopen(readfd, 'r') 342 timedOut = False 343 # Install an alarm handler to interrupt reading the pipe/raise an exception. 344 oldHandler = signal.signal(signal.SIGALRM, self._timeoutException) 345 output = ''; 346 signal.alarm(int(timeout)) 347 try: 348 line = readFile.readline() 349 while line: 350 output += line 351 line = readFile.readline() 352 except SystemExit: 353 timedOut = True 354 signal.alarm(0) 355 if timedOut: 356 os.killpg(childPid, 9) 357 (childPid, status) = os.waitpid(childPid, 0) 358 status = os.WEXITSTATUS(status) 359 if timedOut: 360 status = 1 361 signal.signal(signal.SIGALRM, oldHandler) 362 readFile.close() 363 return (status, output)
364
365 - def printReport(self, verbose=None):
366 """A convenience; prints report(verbose) to stdout.""" 367 print self.report(verbose) + "\n"
368
369 - def processArgv(self, argv):
370 """Processes argv which is a list of command-line arguments of the form 371 -name=value 372 373 The following options are predefined: 374 375 help 376 yes 377 Prints help information describing the reporter inputs, then 378 forces the reporter to exit. If the verbose level is 0, the 379 output will be text; otherwise, it will be Inca Report XML. 380 no (default) 381 Normal reporter execution. 382 383 log 384 0 (default) 385 log no messages 386 1 387 log error messages 388 2 389 log error and warning messages 390 3 391 log error, warning, and system messages 392 4 393 log error, warning, system, and info messages 394 5 395 log error, warning, system, info, and debug messages 396 debug 397 log only debug messages 398 error 399 log only error messages 400 info 401 log only info messages 402 system 403 log only system messages 404 warn 405 log only warning messages 406 407 verbose 408 0 409 print will only produce "completed" or "failed". 410 1 (default) 411 print will produce Inca Report XML. 412 2 413 print will produce Inca Report XML that includes help information. 414 415 version 416 yes 417 Prints the reporter version number and exits. 418 no (default) 419 Normal reporter execution. 420 """ 421 if len(argv) == 1: 422 # we have a single argument; check to see if the input is URL-style query 423 # string, e.g., -file=test.pl&help=no&verbose=1 424 argv = argv[0].split('&') 425 elif len(argv) == 0 and os.environ.has_key('QUERY_STRING'): 426 # maybe we're running as a CGI script 427 argv = os.environ['QUERY_STRING'].split('&') 428 429 argValues = [] 430 badArg = None 431 missing = [] 432 patterns = [] 433 434 for arg in argv: 435 pieces = arg.split('=') 436 name = re.sub('^--?', '', pieces[0]) 437 if len(pieces) == 1: 438 value = 'yes' 439 else: 440 value = pieces[1] 441 if not self.args.has_key(name): 442 badArg = "unknown argument '" + name + "'" 443 elif not re.search(self.args[name]['pat'], value): 444 badArg = "'" + value + "' is not a valid value for -" + name 445 argValues.append(name + '=' + value) 446 self.argv = argValues 447 if badArg != None: 448 self.failPrintAndExit(badArg) 449 450 if self.argValue('help') != 'no': 451 if self.argValue('verbose') == '0': 452 description = self.getDescription() 453 version = self.getVersion() 454 if version == None: 455 version = "No version" 456 url = self.getUrl() 457 if url == None: 458 url = "No URL" 459 text = '' 460 usage = os.path.basename(sys.argv[0]) 461 args = self.args.keys() 462 args.sort() 463 for arg in args: 464 argDefault = self.args[arg]['default'] 465 argDescription = self.args[arg]['description'] 466 text += ' -' + arg + '\n' 467 if argDescription != None: 468 text += '\t' + argDescription + '\n' 469 usage += ' -' + arg 470 if argDefault != None: 471 usage += '=' + str(argDefault) 472 print "NAME:\n " + self.getName() + "\n" + \ 473 "VERSION:\n " + str(version) + "\n" + \ 474 "URL:\n " + url + "\n" + \ 475 "SYNOPSIS:\n " + usage + "\n" + text + "\n" 476 else: 477 print self._reportXml(self._helpXml()) + "\n" 478 sys.exit(0) 479 if self.argValue('version') != 'no': 480 print self.getName() + ' ' + str(self.getVersion()) + "\n" 481 sys.exit(0) 482 483 for arg in self.args.keys(): 484 if self.argValue(arg) == None: 485 missing.append(arg) 486 if len(missing) == 1: 487 self.failPrintAndExit("Missing required argument '" + missing[0] + "'"); 488 elif len(missing) > 0: 489 self.failPrintAndExit \ 490 ("Missing required arguments '" + string.join(missing, "', '") + "'"); 491 492 for arg in self.argValues('log'): 493 if re.match('[012345]$', arg): 494 allTypes = ['error', 'warn', 'system', 'info', 'debug'] 495 arg = string.join(allTypes[0:int(arg)], '|') 496 patterns.append(arg) 497 if len(patterns) > 0: 498 self.log_pat = '^(' + string.join(patterns, '|') + ')$'
499
500 - def report(self, verbose=None):
501 """Returns report text or XML, depending on the value (0, 1, 2) of verbose. 502 Uses the value of the -verbose switch if verbose is None. 503 """ 504 completed = self.getCompleted() 505 msg = self.getFailMessage() 506 if verbose == None: 507 verbose = self.argValue('verbose') 508 if verbose == '0': 509 if completed: 510 result = 'completed' 511 else: 512 result = 'failed' 513 if msg != None: 514 result += ': ' + msg 515 else: 516 if completed and self.getBody() == None: 517 self.setBody(self.reportBody()) 518 if msg != None: 519 messageXml = self.xmlElement('errorMessage', 1, msg) 520 else: 521 messageXml = None 522 if completed: 523 completed = 'true' 524 else: 525 completed = 'false' 526 completedXml = self.xmlElement('completed', 1, completed) 527 if verbose == '2': 528 helpXml = self._helpXml() 529 else: 530 helpXml = None 531 result = self._reportXml( 532 self.xmlElement('body', 0, self.getBody()), 533 self.xmlElement('exitStatus', 0, completedXml, messageXml), 534 helpXml 535 ) 536 return result
537
538 - def reportBody(self):
539 """Constructs and returns the XML contents of the report body. Child 540 classes should override the default implementation, which returns None. 541 """ 542 return None
543
544 - def setBody(self, body):
545 """Sets the body of the report to body.""" 546 self.body = body
547
548 - def setCompleted(self, completed):
549 """Sets the completion indicator of the reporter to completed.""" 550 self.completed = completed
551
552 - def setCwd(self, cwd):
553 """Sets the initial working directory of the reporter to cwd.""" 554 self.cwd = cwd
555
556 - def setDescription(self, description):
557 """Sets the description of the reporter to description.""" 558 self.description = description
559
560 - def setFailMessage(self, msg):
561 """Sets the failure message of the reporter to msg.""" 562 self.fail_message = msg
563
564 - def setName(self, name):
565 """Sets the name that identifies this reporter to name.""" 566 self.name = name
567
568 - def setResult(self, completed, msg=None):
569 """A convenience; calls setCompleted(completed) and setFailMessage(msg).""" 570 self.setCompleted(completed) 571 self.setFailMessage(msg)
572
573 - def setUrl(self, url):
574 """Sets the url for the reporter to url.""" 575 self.url = url
576
577 - def setVersion(self, version):
578 """Sets the version of the reporter to version. Recognizes and parses CVS 579 revision strings. 580 """ 581 if version == None: 582 return 583 found = re.search('Revision: (.*) ', version) 584 if found: 585 self.version = re.group(1) 586 else: 587 self.version = version
588
589 - def tempFile(self, *paths):
590 """A convenience. Adds each element of paths to a list of temporary files 591 that will be deleted automatically when the reporter is destroyed. 592 """ 593 self.temp_paths.extend(paths)
594
595 - def xmlElement(self, name, escape, *contents):
596 """Returns the XML element name surrounding contents. escape should be 597 true only for leaf elements; in this case, each special XML character 598 (<>&) in contents is replaced by the equivalent XML entity. 599 """ 600 innards = '' 601 for content in contents: 602 if content == None: 603 continue 604 content = str(content) 605 if escape: 606 content = re.sub('&', '&amp;', content) 607 content = re.sub('<', '&lt;', content) 608 content = re.sub('>', '&gt;', content) 609 content = re.sub('^<', '\n<', content, 1) 610 innards += content 611 innards = re.sub('(?m)^( *)<', r' \1<', innards) 612 innards = re.sub('>$', '>\n', innards, 1) 613 return '<' + name + '>' + innards + '</' + name + '>'
614
615 - def __del__(self):
616 """Class destructor.""" 617 if len(self.temp_paths) > 0: 618 commands.getoutput( 619 '/bin/rm -fr ' + string.join(self.temp_paths, ' ') 620 )
621
622 - def _helpXml(self):
623 """Returns help information formatted as the body of an Inca report.""" 624 argsAndDepsXml = [] 625 args = self.args.keys() 626 args.sort() 627 for arg in args: 628 info = self.args[arg] 629 defaultXml = info['default'] 630 if defaultXml != None: 631 defaultXml = self.xmlElement('default', 1, defaultXml) 632 description = info['description'] 633 if description == None: 634 description = '' 635 argsAndDepsXml.append(self.xmlElement('argDescription', 0, 636 self.xmlElement('ID', 1, arg), 637 self.xmlElement('accepted', 1, info['pat']), 638 self.xmlElement('description', 1, description), 639 defaultXml 640 )) 641 for dep in self.dependencies: 642 argsAndDepsXml.append( 643 self.xmlElement('dependency', 0, self.xmlElement('ID', 1, dep)) 644 ) 645 return self.xmlElement('help', 0, 646 self.xmlElement('ID', 1, 'help'), 647 self.xmlElement('name', 1, self.getName()), 648 self.xmlElement('version', 1, str(self.getVersion())), 649 self.xmlElement('description', 1, self.getDescription()), 650 self.xmlElement('url', 1, self.getUrl()), \ 651 *argsAndDepsXml 652 )
653
654 - def _iso8601Time(self, when):
655 """Returns the UTC time for the time() return value when in ISO 8601 656 format: CCMM-MM-DDTHH:MM:SSZ 657 """ 658 t = time.gmtime(when) 659 return '%04d-%02d-%02dT%02d:%02d:%02dZ' % \ 660 (t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec)
661
662 - def _reportXml(self, *contents):
663 """Returns XML report beginning with the header and input sections plus any 664 contents specified in the arguments. 665 """ 666 argXmls = [] 667 logXmls = [] 668 hostname = socket.gethostname() 669 if hostname.find('.') < 0: 670 hostname = socket.getfqdn() 671 672 tags = [ 673 self.xmlElement('gmt', 1, self._iso8601Time(time.time())), 674 self.xmlElement('hostname', 1, hostname), 675 self.xmlElement('name', 1, self.getName()), 676 self.xmlElement('version', 1, str(self.getVersion())), 677 self.xmlElement('workingDir', 1, self.getCwd()), 678 self.xmlElement('reporterPath', 1, sys.argv[0]) 679 ] 680 args = self.args.keys() 681 args.sort() 682 for arg in args: 683 valuesXml = [] 684 for value in self.argValues(arg): 685 valuesXml.append(self.xmlElement('value', 1, value)) 686 if len(valuesXml) == 0: 687 valuesXml.append(self.xmlElement('value', 1, '')) 688 argXmls.append \ 689 (self.xmlElement('arg', 0, self.xmlElement('name', 1, arg), *valuesXml)) 690 tags.extend(argXmls) 691 for entry in self.log_entries: 692 logXmls.append(self.xmlElement(entry['type'], 0, 693 self.xmlElement('gmt', 1, self._iso8601Time(entry['time'])), 694 self.xmlElement('message', 1, entry['msg']) 695 )) 696 if len(logXmls) > 0: 697 tags.append(self.xmlElement('log', 0, *logXmls)) 698 tags.extend(contents) 699 result = self.xmlElement('rep:report', 0, *tags) 700 result = re.sub('<rep:report', "<rep:report xmlns:rep='http://inca.sdsc.edu/dataModel/report_2.1'", result, 1) 701 return "<?xml version='1.0'?>\n" + result
702
703 - def _timeoutException(*args):
704 """SIGALRM handler that throws an exception.""" 705 raise SystemExit(1)
706