diff options
Diffstat (limited to 'tools/purify/common.py')
-rw-r--r-- | tools/purify/common.py | 344 |
1 files changed, 0 insertions, 344 deletions
diff --git a/tools/purify/common.py b/tools/purify/common.py deleted file mode 100644 index 7702be0..0000000 --- a/tools/purify/common.py +++ /dev/null @@ -1,344 +0,0 @@ -#!/bin/env python -# Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -# common.py - -""" Common code used by purify_test.py and quantify_test.py in order to automate -running of Rational Purify and Quantify in a consistent manner. -""" - -# Purify and Quantify have a front-end (e.g. quantifyw.exe) which talks to a -# back-end engine (e.g. quantifye.exe). The back-end seems to handle -# instrumentation, while the front-end controls program execution and -# measurement. The front-end will dynamically launch the back-end if -# instrumentation is needed (sometimes in the middle of a run if a dll is -# loaded dynamically). -# In an ideal world, this script would simply execute the front-end and check -# the output. However, purify is not the most reliable or well-documented app -# on the planet, and my attempts to get it to run this way led to the back-end -# engine hanging during instrumentation. The workaround to this was to run two -# passes, first running the engine to do instrumentation rather than letting -# the front-end do it for you, then running the front-end to actually do the -# run. Each time through we're deleting all of the instrumented files in the -# cache to ensure that we're testing that instrumentation works from scratch. -# (although this can be changed with an option) - -import datetime -import logging -import optparse -import os -import signal -import subprocess -import sys -import tempfile -import time - -import google.logging_utils - -# hard-coded location of Rational files and directories -PROGRAMFILES_PATH = os.environ.get('PROGRAMFILES', - os.path.join("C:\\", "Program Files")) -RATIONAL_PATH = os.path.join(PROGRAMFILES_PATH, "Rational") -COMMON_PATH = os.path.join(RATIONAL_PATH, "common") -PPLUS_PATH = os.path.join(RATIONAL_PATH, "PurifyPlus") -PURIFY_PATH = os.path.join(COMMON_PATH, "purify.exe") -PURIFYW_PATH = os.path.join(PPLUS_PATH, "purifyW.exe") -PURIFYE_PATH = os.path.join(PPLUS_PATH, "purifye.exe") -QUANTIFYE_PATH = os.path.join(PPLUS_PATH, "quantifye.exe") -QUANTIFYW_PATH = os.path.join(PPLUS_PATH, "quantifyw.exe") - -class TimeoutError(Exception): pass - - -def _print_line(line, flush=True): - # Printing to a text file (including stdout) on Windows always winds up - # using \r\n automatically. On buildbot, this winds up being read by a master - # running on Linux, so we manually convert crlf to '\n' - print line.rstrip() + '\n', - if flush: - sys.stdout.flush() - -def RunSubprocess(proc, timeout=0, detach=False): - """ Runs a subprocess, until it finishes or |timeout| is exceeded and the - process is killed with taskkill. A |timeout| <= 0 means no timeout. - - Args: - proc: list of process components (exe + args) - timeout: how long to wait before killing, <= 0 means wait forever - detach: Whether to pass the DETACHED_PROCESS argument to CreateProcess - on Windows. This is used by Purify subprocesses on buildbot which - seem to get confused by the parent console that buildbot sets up. - """ - - logging.info("running %s, timeout %d sec" % (" ".join(proc), timeout)) - if detach: - # see MSDN docs for "Process Creation Flags" - DETACHED_PROCESS = 0x8 - p = subprocess.Popen(proc, creationflags=DETACHED_PROCESS) - else: - # For non-detached processes, manually read and print out stdout and stderr. - # By default, the subprocess is supposed to inherit these from its parent, - # however when run under buildbot, it seems unable to read data from a - # grandchild process, so we have to read the child and print the data as if - # it came from us for buildbot to read it. We're not sure why this is - # necessary. - # TODO(erikkay): should we buffer stderr and stdout separately? - p = subprocess.Popen(proc, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - - logging.info("started subprocess") - - # How long to wait (in seconds) before printing progress log messages. - progress_delay = 300 - progress_delay_time = time.time() + progress_delay - did_timeout = False - if timeout > 0: - wait_until = time.time() + timeout - while p.poll() is None and not did_timeout: - if not detach: - line = p.stdout.readline() - while line and not did_timeout: - _print_line(line) - line = p.stdout.readline() - if timeout > 0: - did_timeout = time.time() > wait_until - else: - # When we detach, blocking on reading stdout doesn't work, so we sleep - # a short time and poll. - time.sleep(0.5) - if time.time() >= progress_delay_time: - # Force output on a periodic basis to avoid getting killed off by the - # buildbot. - # TODO(erikkay): I'd prefer a less obtrusive 'print ".",' with a flush - # but because of how we're doing subprocesses, this doesn't appear to - # work reliably. - logging.info("%s still running..." % os.path.basename(proc[0])) - progress_delay_time = time.time() + progress_delay - if timeout > 0: - did_timeout = time.time() > wait_until - - if did_timeout: - logging.info("process timed out") - else: - logging.info("process ended, did not time out") - - if did_timeout: - if sys.platform == "win32": - subprocess.call(["taskkill", "/T", "/F", "/PID", str(p.pid)]) - else: - # Does this kill all children, too? - os.kill(p.pid, signal.SIGINT) - logging.error("KILLED %d" % p.pid) - # Give the process a chance to actually die before continuing - # so that cleanup can happen safely. - time.sleep(1.0) - logging.error("TIMEOUT waiting for %s" % proc[0]) - raise TimeoutError(proc[0]) - elif not detach: - for line in p.stdout.readlines(): - _print_line(line, False) - if sys.platform != 'darwin': # stdout flush fails on Mac - logging.info("flushing stdout") - p.stdout.flush() - - logging.info("collecting result code") - result = p.poll() - if result: - logging.error("%s exited with non-zero result code %d" % (proc[0], result)) - return result - - -def FixPath(path): - """We pass computed paths to Rational as arguments, so these paths must be - valid windows paths. When running in cygwin's python, computed paths - wind up looking like /cygdrive/c/..., so we need to call out to cygpath - to fix them up. - """ - if sys.platform != "cygwin": - return path - p = subprocess.Popen(["cygpath", "-a", "-m", path], stdout=subprocess.PIPE) - return p.communicate()[0].rstrip() - - -class Rational(object): - ''' Common superclass for Purify and Quantify automation objects. Handles - common argument parsing as well as the general program flow of Instrument, - Execute, Analyze. - ''' - - def __init__(self): - google.logging_utils.config_root() - self._out_file = None - - def Run(self): - '''Call this to run through the whole process: - Setup, Instrument, Execute, Analyze''' - start = datetime.datetime.now() - retcode = -1 - if self.Setup(): - retcode = self._Run() - self.Cleanup() - else: - logging.error("Setup failed") - end = datetime.datetime.now() - seconds = (end - start).seconds - hours = seconds / 3600 - seconds = seconds % 3600 - minutes = seconds / 60 - seconds = seconds % 60 - logging.info("elapsed time: %02d:%02d:%02d" % (hours, minutes, seconds)) - return retcode - - def _Run(self): - retcode = -1 - if not self.Instrument(): - logging.error("Instrumentation failed.") - return retcode - if self._instrument_only: - logging.info("Instrumentation completed successfully.") - return 0 - if not self.Execute(): - logging.error("Execute failed.") - return - retcode = self.Analyze() - if retcode: - logging.error("Analyze failed.") - return retcode - logging.info("Instrumentation and execution completed successfully.") - return 0 - - def CreateOptionParser(self): - '''Creates OptionParser with shared arguments. Overridden by subclassers - to add custom arguments.''' - parser = optparse.OptionParser("usage: %prog [options] <program to test>") - # since the trailing program likely has command-line args of itself - # we need to stop parsing when we reach the first positional arg - parser.disable_interspersed_args() - parser.add_option("-o", "--out_file", dest="out_file", metavar="OUTFILE", - default="", - help="output data is written to OUTFILE") - parser.add_option("-s", "--save_cache", - dest="save_cache", action="store_true", default=False, - help="don't delete instrumentation cache") - parser.add_option("-c", "--cache_dir", dest="cache_dir", metavar="CACHEDIR", - default="", - help="location of instrumentation cache is CACHEDIR") - parser.add_option("-m", "--manual", - dest="manual_run", action="store_true", default=False, - help="target app is being run manually, don't timeout") - parser.add_option("-t", "--timeout", - dest="timeout", metavar="TIMEOUT", default=10000, - help="timeout in seconds for the run (default 10000)") - parser.add_option("-v", "--verbose", action="store_true", default=False, - help="verbose output - enable debug log messages") - parser.add_option("", "--instrument_only", action="store_true", - default=False, - help="Only instrument the target without running") - self._parser = parser - - def Setup(self): - if self.ParseArgv(): - logging.info("instrumentation cache in %s" % self._cache_dir) - logging.info("output saving to %s" % self._out_file) - # Ensure that Rational's common dir and cache dir are in the front of the - # path. The common dir is required for purify to run in any case, and - # the cache_dir is required when using the /Replace=yes option. - os.environ["PATH"] = (COMMON_PATH + ";" + self._cache_dir + ";" + - os.environ["PATH"]) - # clear the cache to make sure we're starting clean - self.__ClearInstrumentationCache() - return True - return False - - def Instrument(self, proc): - '''Instrument the app to be tested. Full instrumentation command-line - provided by subclassers via proc.''' - logging.info("starting instrumentation...") - if RunSubprocess(proc, self._timeout, detach=True) == 0: - if "/Replace=yes" in proc: - if os.path.exists(self._exe + ".Original"): - return True - elif self._instrument_only: - # TODO(paulg): Catch instrumentation errors and clean up properly. - return True - elif os.path.isdir(self._cache_dir): - for cfile in os.listdir(self._cache_dir): - # TODO(erikkay): look for the actual munged purify filename - ext = os.path.splitext(cfile)[1] - if ext == ".exe": - return True - logging.error("no instrumentation data generated") - return False - - def Execute(self, proc): - ''' Execute the app to be tested after successful instrumentation. - Full execution command-line provided by subclassers via proc.''' - logging.info("starting execution...") - # note that self._args begins with the exe to be run - proc += self._args - if RunSubprocess(proc, self._timeout) == 0: - return True - return False - - def Analyze(self): - '''Analyze step after a successful Execution. Should be overridden - by the subclasser if instrumentation is desired. - Returns 0 for success, 88 for warning (see ReturnCodeCommand) and anything - else for error - ''' - return -1 - - def ParseArgv(self): - '''Parses arguments according to CreateOptionParser - Subclassers must override if they have extra arguments.''' - self.CreateOptionParser() - self._options, self._args = self._parser.parse_args() - if self._options.verbose: - google.logging_utils.config_root(logging.DEBUG) - self._save_cache = self._options.save_cache - self._manual_run = self._options.manual_run - if self._manual_run: - logging.info("manual run - timeout disabled") - self._timeout = 0 - else: - self._timeout = int(self._options.timeout) - logging.info("timeout set to %ds" % (self._timeout)) - if self._save_cache: - logging.info("saving instrumentation cache") - if not self._options.cache_dir: - try: - temp_dir = os.environ["TEMP"] - except KeyError: - temp_dir = tempfile.mkdtemp() - self._cache_dir = os.path.join(FixPath(temp_dir), - "instrumentation_cache") - else: - self._cache_dir = FixPath(os.path.abspath(self._options.cache_dir)) - if self._options.out_file: - self._out_file = FixPath(os.path.abspath(self._options.out_file)) - if len(self._args) == 0: - self._parser.error("missing program to %s" % (self.__class__.__name__,)) - return False - self._exe = self._args[0] - self._exe_dir = FixPath(os.path.abspath(os.path.dirname(self._exe))) - self._instrument_only = self._options.instrument_only - return True - - def Cleanup(self): - # delete the cache to avoid filling up the hard drive when we're using - # temporary directory names - self.__ClearInstrumentationCache() - - def __ClearInstrumentationCache(self): - if not self._save_cache: - logging.info("clearing instrumentation cache %s" % self._cache_dir) - if os.path.isdir(self._cache_dir): - for cfile in os.listdir(self._cache_dir): - file = os.path.join(self._cache_dir, cfile) - if os.path.isfile(file): - try: - os.remove(file) - except: - logging.warning("unable to delete file %s: %s" % (file, - sys.exc_info()[0])) |