forked from OSchip/llvm-project
Remove old test runner, this has moved to LLVM/utils/lit and all known clients
have been updated. - Please let me know of any problems. llvm-svn: 82524
This commit is contained in:
parent
f9539d0c3f
commit
b5cbf77c2e
|
|
@ -8,8 +8,8 @@ else
|
|||
TESTDIRS ?= $(PROJ_SRC_DIR)
|
||||
endif
|
||||
|
||||
# LIT2 wants objdir paths, so it will pick up the lit.site.cfg.
|
||||
LIT2_TESTDIRS := $(TESTDIRS:$(PROJ_SRC_DIR)%=$(PROJ_OBJ_DIR)%)
|
||||
# 'lit' wants objdir paths, so it will pick up the lit.site.cfg.
|
||||
TESTDIRS := $(TESTDIRS:$(PROJ_SRC_DIR)%=$(PROJ_OBJ_DIR)%)
|
||||
|
||||
ifndef TESTARGS
|
||||
ifdef VERBOSE
|
||||
|
|
@ -25,20 +25,10 @@ else
|
|||
VGARG=
|
||||
endif
|
||||
|
||||
ifndef LIT1
|
||||
all:: lit.site.cfg
|
||||
@ echo '--- Running clang tests for $(TARGET_TRIPLE) ---'
|
||||
@ $(LLVM_SRC_ROOT)/utils/lit/lit.py \
|
||||
$(TESTARGS) $(LIT2_TESTDIRS) $(VGARG)
|
||||
else
|
||||
all::
|
||||
@ echo '--- Running clang tests for $(TARGET_TRIPLE) ---'
|
||||
@ $(PROJ_SRC_DIR)/../utils/test/MultiTestRunner.py \
|
||||
--root $(PROJ_SRC_DIR) \
|
||||
--path $(ToolDir) \
|
||||
--path $(LLVM_SRC_ROOT)/test/Scripts \
|
||||
$(TESTARGS) $(TESTDIRS) $(VGARG)
|
||||
endif
|
||||
|
||||
FORCE:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,166 +1,146 @@
|
|||
# -*- Python -*-
|
||||
|
||||
def config_new():
|
||||
import os
|
||||
|
||||
# Configuration file for the 'lit' test runner.
|
||||
|
||||
# name: The name of this test suite.
|
||||
config.name = 'Clang'
|
||||
|
||||
# testFormat: The test format to use to interpret tests.
|
||||
#
|
||||
# For now we require '&&' between commands, until they get globally killed and
|
||||
# the test runner updated.
|
||||
config.test_format = lit.formats.ShTest(execute_external = True,
|
||||
require_and_and = True)
|
||||
|
||||
# suffixes: A list of file extensions to treat as test files.
|
||||
config.suffixes = ['.c', '.cpp', '.m', '.mm']
|
||||
|
||||
# test_source_root: The root path where tests are located.
|
||||
config.test_source_root = os.path.dirname(__file__)
|
||||
|
||||
# test_exec_root: The root path where tests should be run.
|
||||
clang_obj_root = getattr(config, 'clang_obj_root', None)
|
||||
if clang_obj_root is not None:
|
||||
config.test_exec_root = os.path.join(clang_obj_root, 'test')
|
||||
|
||||
# Set llvm_{src,obj}_root for use by others.
|
||||
config.llvm_src_root = getattr(config, 'llvm_src_root', None)
|
||||
config.llvm_obj_root = getattr(config, 'llvm_obj_root', None)
|
||||
|
||||
# Tweak the PATH to include the tools dir and the scripts dir.
|
||||
if clang_obj_root is not None:
|
||||
llvm_tools_dir = getattr(config, 'llvm_tools_dir', None)
|
||||
if not llvm_tools_dir:
|
||||
lit.fatal('No LLVM tools dir set!')
|
||||
path = os.path.pathsep.join((llvm_tools_dir,
|
||||
os.path.join(config.llvm_src_root,
|
||||
'test', 'Scripts'),
|
||||
config.environment['PATH']))
|
||||
config.environment['PATH'] = path
|
||||
|
||||
###
|
||||
|
||||
# Check that the object root is known.
|
||||
if config.test_exec_root is None:
|
||||
# Otherwise, we haven't loaded the site specific configuration (the user is
|
||||
# probably trying to run on a test file directly, and either the site
|
||||
# configuration hasn't been created by the build system, or we are in an
|
||||
# out-of-tree build situation).
|
||||
|
||||
# Try to detect the situation where we are using an out-of-tree build by
|
||||
# looking for 'llvm-config'.
|
||||
#
|
||||
# FIXME: I debated (i.e., wrote and threw away) adding logic to
|
||||
# automagically generate the lit.site.cfg if we are in some kind of fresh
|
||||
# build situation. This means knowing how to invoke the build system
|
||||
# though, and I decided it was too much magic.
|
||||
|
||||
llvm_config = lit.util.which('llvm-config', config.environment['PATH'])
|
||||
if not llvm_config:
|
||||
lit.fatal('No site specific configuration available!')
|
||||
|
||||
# Get the source and object roots.
|
||||
llvm_src_root = lit.util.capture(['llvm-config', '--src-root']).strip()
|
||||
llvm_obj_root = lit.util.capture(['llvm-config', '--obj-root']).strip()
|
||||
clang_src_root = os.path.join(llvm_src_root, "tools", "clang")
|
||||
clang_obj_root = os.path.join(llvm_obj_root, "tools", "clang")
|
||||
|
||||
# Validate that we got a tree which points to here, using the standard
|
||||
# tools/clang layout.
|
||||
this_src_root = os.path.dirname(config.test_source_root)
|
||||
if os.path.realpath(clang_src_root) != os.path.realpath(this_src_root):
|
||||
lit.fatal('No site specific configuration available!')
|
||||
|
||||
# Check that the site specific configuration exists.
|
||||
site_cfg = os.path.join(clang_obj_root, 'test', 'lit.site.cfg')
|
||||
if not os.path.exists(site_cfg):
|
||||
lit.fatal('No site specific configuration available!')
|
||||
|
||||
# Okay, that worked. Notify the user of the automagic, and reconfigure.
|
||||
lit.note('using out-of-tree build at %r' % clang_obj_root)
|
||||
lit.load_config(config, site_cfg)
|
||||
raise SystemExit
|
||||
|
||||
###
|
||||
|
||||
# Discover the 'clang' and 'clangcc' to use.
|
||||
|
||||
import os
|
||||
|
||||
def inferClang(PATH):
|
||||
# Determine which clang to use.
|
||||
clang = os.getenv('CLANG')
|
||||
|
||||
# If the user set clang in the environment, definitely use that and don't
|
||||
# try to validate.
|
||||
if clang:
|
||||
return clang
|
||||
|
||||
# Otherwise look in the path.
|
||||
clang = lit.util.which('clang', PATH)
|
||||
|
||||
if not clang:
|
||||
lit.fatal("couldn't find 'clang' program, try setting "
|
||||
"CLANG in your environment")
|
||||
|
||||
return clang
|
||||
|
||||
def inferClangCC(clang, PATH):
|
||||
clangcc = os.getenv('CLANGCC')
|
||||
|
||||
# If the user set clang in the environment, definitely use that and don't
|
||||
# try to validate.
|
||||
if clangcc:
|
||||
return clangcc
|
||||
|
||||
# Otherwise try adding -cc since we expect to be looking in a build
|
||||
# directory.
|
||||
if clang.endswith('.exe'):
|
||||
clangccName = clang[:-4] + '-cc.exe'
|
||||
else:
|
||||
clangccName = clang + '-cc'
|
||||
clangcc = lit.util.which(clangccName, PATH)
|
||||
if not clangcc:
|
||||
# Otherwise ask clang.
|
||||
res = lit.util.capture([clang, '-print-prog-name=clang-cc'])
|
||||
res = res.strip()
|
||||
if res and os.path.exists(res):
|
||||
clangcc = res
|
||||
|
||||
if not clangcc:
|
||||
lit.fatal("couldn't find 'clang-cc' program, try setting "
|
||||
"CLANGCC in your environment")
|
||||
|
||||
return clangcc
|
||||
|
||||
config.clang = inferClang(config.environment['PATH'])
|
||||
if not lit.quiet:
|
||||
lit.note('using clang: %r' % config.clang)
|
||||
config.substitutions.append( (' clang ', ' ' + config.clang + ' ') )
|
||||
|
||||
config.clang_cc = inferClangCC(config.clang, config.environment['PATH'])
|
||||
if not lit.quiet:
|
||||
lit.note('using clang-cc: %r' % config.clang_cc)
|
||||
config.substitutions.append( (' clang-cc ', ' ' + config.clang_cc + ' ') )
|
||||
|
||||
if 'config' in globals():
|
||||
config_new()
|
||||
raise SystemExit # End configuration.
|
||||
import os
|
||||
|
||||
# Configuration file for the 'lit' test runner.
|
||||
|
||||
# suffixes: A list of file extensions to treat as test files.
|
||||
suffixes = ['.c', '.cpp', '.m', '.mm']
|
||||
# name: The name of this test suite.
|
||||
config.name = 'Clang'
|
||||
|
||||
# environment: The base environment to use when running test commands.
|
||||
# testFormat: The test format to use to interpret tests.
|
||||
#
|
||||
# The 'PATH' and 'SYSTEMROOT' variables will be set automatically from the lit
|
||||
# command line variables.
|
||||
environment = {}
|
||||
# For now we require '&&' between commands, until they get globally killed and
|
||||
# the test runner updated.
|
||||
config.test_format = lit.formats.ShTest(execute_external = True,
|
||||
require_and_and = True)
|
||||
|
||||
# requireAndAnd: Require '&&' between commands, until they get globally killed
|
||||
# and the test runner updated.
|
||||
requireAndAnd = True
|
||||
# suffixes: A list of file extensions to treat as test files.
|
||||
config.suffixes = ['.c', '.cpp', '.m', '.mm']
|
||||
|
||||
# test_source_root: The root path where tests are located.
|
||||
config.test_source_root = os.path.dirname(__file__)
|
||||
|
||||
# test_exec_root: The root path where tests should be run.
|
||||
clang_obj_root = getattr(config, 'clang_obj_root', None)
|
||||
if clang_obj_root is not None:
|
||||
config.test_exec_root = os.path.join(clang_obj_root, 'test')
|
||||
|
||||
# Set llvm_{src,obj}_root for use by others.
|
||||
config.llvm_src_root = getattr(config, 'llvm_src_root', None)
|
||||
config.llvm_obj_root = getattr(config, 'llvm_obj_root', None)
|
||||
|
||||
# Tweak the PATH to include the tools dir and the scripts dir.
|
||||
if clang_obj_root is not None:
|
||||
llvm_tools_dir = getattr(config, 'llvm_tools_dir', None)
|
||||
if not llvm_tools_dir:
|
||||
lit.fatal('No LLVM tools dir set!')
|
||||
path = os.path.pathsep.join((llvm_tools_dir,
|
||||
os.path.join(config.llvm_src_root,
|
||||
'test', 'Scripts'),
|
||||
config.environment['PATH']))
|
||||
config.environment['PATH'] = path
|
||||
|
||||
###
|
||||
|
||||
# Check that the object root is known.
|
||||
if config.test_exec_root is None:
|
||||
# Otherwise, we haven't loaded the site specific configuration (the user is
|
||||
# probably trying to run on a test file directly, and either the site
|
||||
# configuration hasn't been created by the build system, or we are in an
|
||||
# out-of-tree build situation).
|
||||
|
||||
# Try to detect the situation where we are using an out-of-tree build by
|
||||
# looking for 'llvm-config'.
|
||||
#
|
||||
# FIXME: I debated (i.e., wrote and threw away) adding logic to
|
||||
# automagically generate the lit.site.cfg if we are in some kind of fresh
|
||||
# build situation. This means knowing how to invoke the build system
|
||||
# though, and I decided it was too much magic.
|
||||
|
||||
llvm_config = lit.util.which('llvm-config', config.environment['PATH'])
|
||||
if not llvm_config:
|
||||
lit.fatal('No site specific configuration available!')
|
||||
|
||||
# Get the source and object roots.
|
||||
llvm_src_root = lit.util.capture(['llvm-config', '--src-root']).strip()
|
||||
llvm_obj_root = lit.util.capture(['llvm-config', '--obj-root']).strip()
|
||||
clang_src_root = os.path.join(llvm_src_root, "tools", "clang")
|
||||
clang_obj_root = os.path.join(llvm_obj_root, "tools", "clang")
|
||||
|
||||
# Validate that we got a tree which points to here, using the standard
|
||||
# tools/clang layout.
|
||||
this_src_root = os.path.dirname(config.test_source_root)
|
||||
if os.path.realpath(clang_src_root) != os.path.realpath(this_src_root):
|
||||
lit.fatal('No site specific configuration available!')
|
||||
|
||||
# Check that the site specific configuration exists.
|
||||
site_cfg = os.path.join(clang_obj_root, 'test', 'lit.site.cfg')
|
||||
if not os.path.exists(site_cfg):
|
||||
lit.fatal('No site specific configuration available!')
|
||||
|
||||
# Okay, that worked. Notify the user of the automagic, and reconfigure.
|
||||
lit.note('using out-of-tree build at %r' % clang_obj_root)
|
||||
lit.load_config(config, site_cfg)
|
||||
raise SystemExit
|
||||
|
||||
###
|
||||
|
||||
# Discover the 'clang' and 'clangcc' to use.
|
||||
|
||||
import os
|
||||
|
||||
def inferClang(PATH):
|
||||
# Determine which clang to use.
|
||||
clang = os.getenv('CLANG')
|
||||
|
||||
# If the user set clang in the environment, definitely use that and don't
|
||||
# try to validate.
|
||||
if clang:
|
||||
return clang
|
||||
|
||||
# Otherwise look in the path.
|
||||
clang = lit.util.which('clang', PATH)
|
||||
|
||||
if not clang:
|
||||
lit.fatal("couldn't find 'clang' program, try setting "
|
||||
"CLANG in your environment")
|
||||
|
||||
return clang
|
||||
|
||||
def inferClangCC(clang, PATH):
|
||||
clangcc = os.getenv('CLANGCC')
|
||||
|
||||
# If the user set clang in the environment, definitely use that and don't
|
||||
# try to validate.
|
||||
if clangcc:
|
||||
return clangcc
|
||||
|
||||
# Otherwise try adding -cc since we expect to be looking in a build
|
||||
# directory.
|
||||
if clang.endswith('.exe'):
|
||||
clangccName = clang[:-4] + '-cc.exe'
|
||||
else:
|
||||
clangccName = clang + '-cc'
|
||||
clangcc = lit.util.which(clangccName, PATH)
|
||||
if not clangcc:
|
||||
# Otherwise ask clang.
|
||||
res = lit.util.capture([clang, '-print-prog-name=clang-cc'])
|
||||
res = res.strip()
|
||||
if res and os.path.exists(res):
|
||||
clangcc = res
|
||||
|
||||
if not clangcc:
|
||||
lit.fatal("couldn't find 'clang-cc' program, try setting "
|
||||
"CLANGCC in your environment")
|
||||
|
||||
return clangcc
|
||||
|
||||
config.clang = inferClang(config.environment['PATH'])
|
||||
if not lit.quiet:
|
||||
lit.note('using clang: %r' % config.clang)
|
||||
config.substitutions.append( (' clang ', ' ' + config.clang + ' ') )
|
||||
|
||||
config.clang_cc = inferClangCC(config.clang, config.environment['PATH'])
|
||||
if not lit.quiet:
|
||||
lit.note('using clang-cc: %r' % config.clang_cc)
|
||||
config.substitutions.append( (' clang-cc ', ' ' + config.clang_cc + ' ') )
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
// RUN: echo 'I am some stdout' &&
|
||||
// RUN: echo 'I am some stderr' 1>&2 &&
|
||||
// RUN: false
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
# -*- Python -*-
|
||||
|
||||
# Configuration file for the 'lit' test runner.
|
||||
|
||||
# suffixes: A list of file extensions to treat as test files.
|
||||
suffixes = ['.c', '.cpp', '.m', '.mm']
|
||||
|
||||
# environment: The base environment to use when running test commands.
|
||||
#
|
||||
# The 'PATH' and 'SYSTEMROOT' variables will be set automatically from the lit
|
||||
# command line variables.
|
||||
environment = {}
|
||||
|
|
@ -1 +0,0 @@
|
|||
// RUN: true
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
// RUN: false
|
||||
// XFAIL
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
// RUN: true
|
||||
// XFAIL
|
||||
|
|
@ -1,399 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
MultiTestRunner - Harness for running multiple tests in the simple clang style.
|
||||
|
||||
TODO
|
||||
--
|
||||
- Use configuration file for clang specific stuff
|
||||
- Use a timeout / ulimit
|
||||
- Detect signaled failures (abort)
|
||||
- Better support for finding tests
|
||||
|
||||
- Support "disabling" tests? The advantage of making this distinct from XFAIL
|
||||
is it makes it more obvious that it is a temporary measure (and MTR can put
|
||||
in a separate category).
|
||||
"""
|
||||
|
||||
import os, sys, re, random, time
|
||||
import threading
|
||||
from Queue import Queue
|
||||
|
||||
import ProgressBar
|
||||
import TestRunner
|
||||
import Util
|
||||
|
||||
from TestingConfig import TestingConfig
|
||||
from TestRunner import TestStatus
|
||||
|
||||
kConfigName = 'lit.cfg'
|
||||
|
||||
def getTests(cfg, inputs):
|
||||
for path in inputs:
|
||||
if not os.path.exists(path):
|
||||
Util.warning('Invalid test %r' % path)
|
||||
continue
|
||||
|
||||
if not os.path.isdir(path):
|
||||
yield path
|
||||
continue
|
||||
|
||||
foundOne = False
|
||||
for dirpath,dirnames,filenames in os.walk(path):
|
||||
# FIXME: This doesn't belong here
|
||||
if 'Output' in dirnames:
|
||||
dirnames.remove('Output')
|
||||
for f in filenames:
|
||||
base,ext = os.path.splitext(f)
|
||||
if ext in cfg.suffixes:
|
||||
yield os.path.join(dirpath,f)
|
||||
foundOne = True
|
||||
if not foundOne:
|
||||
Util.warning('No tests in input directory %r' % path)
|
||||
|
||||
class TestingProgressDisplay:
|
||||
def __init__(self, opts, numTests, progressBar=None):
|
||||
self.opts = opts
|
||||
self.numTests = numTests
|
||||
self.digits = len(str(self.numTests))
|
||||
self.current = None
|
||||
self.lock = threading.Lock()
|
||||
self.progressBar = progressBar
|
||||
self.progress = 0.
|
||||
|
||||
def update(self, index, tr):
|
||||
# Avoid locking overhead in quiet mode
|
||||
if self.opts.quiet and not tr.failed():
|
||||
return
|
||||
|
||||
# Output lock
|
||||
self.lock.acquire()
|
||||
try:
|
||||
self.handleUpdate(index, tr)
|
||||
finally:
|
||||
self.lock.release()
|
||||
|
||||
def finish(self):
|
||||
if self.progressBar:
|
||||
self.progressBar.clear()
|
||||
elif self.opts.succinct:
|
||||
sys.stdout.write('\n')
|
||||
|
||||
def handleUpdate(self, index, tr):
|
||||
if self.progressBar:
|
||||
if tr.failed():
|
||||
self.progressBar.clear()
|
||||
else:
|
||||
# Force monotonicity
|
||||
self.progress = max(self.progress, float(index)/self.numTests)
|
||||
self.progressBar.update(self.progress, tr.path)
|
||||
return
|
||||
elif self.opts.succinct:
|
||||
if not tr.failed():
|
||||
sys.stdout.write('.')
|
||||
sys.stdout.flush()
|
||||
return
|
||||
else:
|
||||
sys.stdout.write('\n')
|
||||
|
||||
status = TestStatus.getName(tr.code).upper()
|
||||
print '%s: %s (%*d of %*d)' % (status, tr.path,
|
||||
self.digits, index+1,
|
||||
self.digits, self.numTests)
|
||||
|
||||
if tr.failed() and self.opts.showOutput:
|
||||
print "%s TEST '%s' FAILED %s" % ('*'*20, tr.path, '*'*20)
|
||||
print tr.output
|
||||
print "*" * 20
|
||||
|
||||
sys.stdout.flush()
|
||||
|
||||
class TestResult:
|
||||
def __init__(self, path, code, output, elapsed):
|
||||
self.path = path
|
||||
self.code = code
|
||||
self.output = output
|
||||
self.elapsed = elapsed
|
||||
|
||||
def failed(self):
|
||||
return self.code in (TestStatus.Fail,TestStatus.XPass)
|
||||
|
||||
class TestProvider:
|
||||
def __init__(self, config, opts, tests, display):
|
||||
self.config = config
|
||||
self.opts = opts
|
||||
self.tests = tests
|
||||
self.index = 0
|
||||
self.lock = threading.Lock()
|
||||
self.results = [None]*len(self.tests)
|
||||
self.startTime = time.time()
|
||||
self.progress = display
|
||||
|
||||
def get(self):
|
||||
self.lock.acquire()
|
||||
try:
|
||||
if self.opts.maxTime is not None:
|
||||
if time.time() - self.startTime > self.opts.maxTime:
|
||||
return None
|
||||
if self.index >= len(self.tests):
|
||||
return None
|
||||
item = self.tests[self.index],self.index
|
||||
self.index += 1
|
||||
return item
|
||||
finally:
|
||||
self.lock.release()
|
||||
|
||||
def setResult(self, index, result):
|
||||
self.results[index] = result
|
||||
self.progress.update(index, result)
|
||||
|
||||
class Tester(threading.Thread):
|
||||
def __init__(self, provider):
|
||||
threading.Thread.__init__(self)
|
||||
self.provider = provider
|
||||
|
||||
def run(self):
|
||||
while 1:
|
||||
item = self.provider.get()
|
||||
if item is None:
|
||||
break
|
||||
self.runTest(item)
|
||||
|
||||
def runTest(self, (path, index)):
|
||||
base = TestRunner.getTestOutputBase('Output', path)
|
||||
numTests = len(self.provider.tests)
|
||||
digits = len(str(numTests))
|
||||
code = None
|
||||
elapsed = None
|
||||
try:
|
||||
opts = self.provider.opts
|
||||
startTime = time.time()
|
||||
code, output = TestRunner.runOneTest(self.provider.config,
|
||||
path, base)
|
||||
elapsed = time.time() - startTime
|
||||
except KeyboardInterrupt:
|
||||
# This is a sad hack. Unfortunately subprocess goes
|
||||
# bonkers with ctrl-c and we start forking merrily.
|
||||
print '\nCtrl-C detected, goodbye.'
|
||||
os.kill(0,9)
|
||||
|
||||
self.provider.setResult(index, TestResult(path, code, output, elapsed))
|
||||
|
||||
def findConfigPath(root):
|
||||
prev = None
|
||||
while root != prev:
|
||||
cfg = os.path.join(root, kConfigName)
|
||||
if os.path.exists(cfg):
|
||||
return cfg
|
||||
|
||||
prev,root = root,os.path.dirname(root)
|
||||
|
||||
raise ValueError,"Unable to find config file %r" % kConfigName
|
||||
|
||||
def runTests(opts, provider):
|
||||
# If only using one testing thread, don't use threads at all; this lets us
|
||||
# profile, among other things.
|
||||
if opts.numThreads == 1:
|
||||
t = Tester(provider)
|
||||
t.run()
|
||||
return
|
||||
|
||||
# Otherwise spin up the testing threads and wait for them to finish.
|
||||
testers = [Tester(provider) for i in range(opts.numThreads)]
|
||||
for t in testers:
|
||||
t.start()
|
||||
try:
|
||||
for t in testers:
|
||||
t.join()
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(1)
|
||||
|
||||
def main():
|
||||
global options
|
||||
from optparse import OptionParser, OptionGroup
|
||||
parser = OptionParser("usage: %prog [options] {file-or-path}")
|
||||
|
||||
parser.add_option("", "--root", dest="root",
|
||||
help="Path to root test directory",
|
||||
action="store", default=None)
|
||||
parser.add_option("", "--config", dest="config",
|
||||
help="Testing configuration file [default='%s']" % kConfigName,
|
||||
action="store", default=None)
|
||||
|
||||
group = OptionGroup(parser, "Output Format")
|
||||
# FIXME: I find these names very confusing, although I like the
|
||||
# functionality.
|
||||
group.add_option("-q", "--quiet", dest="quiet",
|
||||
help="Suppress no error output",
|
||||
action="store_true", default=False)
|
||||
group.add_option("-s", "--succinct", dest="succinct",
|
||||
help="Reduce amount of output",
|
||||
action="store_true", default=False)
|
||||
group.add_option("-v", "--verbose", dest="showOutput",
|
||||
help="Show all test output",
|
||||
action="store_true", default=False)
|
||||
group.add_option("", "--no-progress-bar", dest="useProgressBar",
|
||||
help="Do not use curses based progress bar",
|
||||
action="store_false", default=True)
|
||||
parser.add_option_group(group)
|
||||
|
||||
group = OptionGroup(parser, "Test Execution")
|
||||
group.add_option("-j", "--threads", dest="numThreads",
|
||||
help="Number of testing threads",
|
||||
type=int, action="store",
|
||||
default=None)
|
||||
group.add_option("", "--clang", dest="clang",
|
||||
help="Program to use as \"clang\"",
|
||||
action="store", default=None)
|
||||
group.add_option("", "--clang-cc", dest="clangcc",
|
||||
help="Program to use as \"clang-cc\"",
|
||||
action="store", default=None)
|
||||
group.add_option("", "--path", dest="path",
|
||||
help="Additional paths to add to testing environment",
|
||||
action="append", type=str, default=[])
|
||||
group.add_option("", "--no-sh", dest="useExternalShell",
|
||||
help="Run tests using an external shell",
|
||||
action="store_false", default=True)
|
||||
group.add_option("", "--vg", dest="useValgrind",
|
||||
help="Run tests under valgrind",
|
||||
action="store_true", default=False)
|
||||
group.add_option("", "--vg-arg", dest="valgrindArgs",
|
||||
help="Specify an extra argument for valgrind",
|
||||
type=str, action="append", default=[])
|
||||
group.add_option("", "--time-tests", dest="timeTests",
|
||||
help="Track elapsed wall time for each test",
|
||||
action="store_true", default=False)
|
||||
parser.add_option_group(group)
|
||||
|
||||
group = OptionGroup(parser, "Test Selection")
|
||||
group.add_option("", "--max-tests", dest="maxTests",
|
||||
help="Maximum number of tests to run",
|
||||
action="store", type=int, default=None)
|
||||
group.add_option("", "--max-time", dest="maxTime",
|
||||
help="Maximum time to spend testing (in seconds)",
|
||||
action="store", type=float, default=None)
|
||||
group.add_option("", "--shuffle", dest="shuffle",
|
||||
help="Run tests in random order",
|
||||
action="store_true", default=False)
|
||||
parser.add_option_group(group)
|
||||
|
||||
(opts, args) = parser.parse_args()
|
||||
|
||||
if not args:
|
||||
parser.error('No inputs specified')
|
||||
|
||||
if opts.numThreads is None:
|
||||
opts.numThreads = Util.detectCPUs()
|
||||
|
||||
inputs = args
|
||||
|
||||
# Resolve root if not given, either infer it from the config file if given,
|
||||
# otherwise from the inputs.
|
||||
if not opts.root:
|
||||
if opts.config:
|
||||
opts.root = os.path.dirname(opts.config)
|
||||
else:
|
||||
opts.root = os.path.commonprefix([os.path.abspath(p)
|
||||
for p in inputs])
|
||||
|
||||
# Find the config file, if not specified.
|
||||
if not opts.config:
|
||||
try:
|
||||
opts.config = findConfigPath(opts.root)
|
||||
except ValueError,e:
|
||||
parser.error(e.args[0])
|
||||
|
||||
cfg = TestingConfig.frompath(opts.config)
|
||||
|
||||
# Update the configuration based on the command line arguments.
|
||||
for name in ('PATH','SYSTEMROOT'):
|
||||
if name in cfg.environment:
|
||||
parser.error("'%s' should not be set in configuration!" % name)
|
||||
|
||||
cfg.root = opts.root
|
||||
cfg.environment['PATH'] = os.pathsep.join(opts.path +
|
||||
[os.environ.get('PATH','')])
|
||||
cfg.environment['SYSTEMROOT'] = os.environ.get('SYSTEMROOT','')
|
||||
|
||||
if opts.clang is None:
|
||||
opts.clang = TestRunner.inferClang(cfg)
|
||||
if opts.clangcc is None:
|
||||
opts.clangcc = TestRunner.inferClangCC(cfg, opts.clang)
|
||||
|
||||
cfg.clang = opts.clang
|
||||
cfg.clangcc = opts.clangcc
|
||||
cfg.useValgrind = opts.useValgrind
|
||||
cfg.valgrindArgs = opts.valgrindArgs
|
||||
cfg.useExternalShell = opts.useExternalShell
|
||||
|
||||
# FIXME: It could be worth loading these in parallel with testing.
|
||||
allTests = list(getTests(cfg, args))
|
||||
allTests.sort()
|
||||
|
||||
tests = allTests
|
||||
if opts.shuffle:
|
||||
random.shuffle(tests)
|
||||
if opts.maxTests is not None:
|
||||
tests = tests[:opts.maxTests]
|
||||
|
||||
extra = ''
|
||||
if len(tests) != len(allTests):
|
||||
extra = ' of %d'%(len(allTests),)
|
||||
header = '-- Testing: %d%s tests, %d threads --'%(len(tests),extra,
|
||||
opts.numThreads)
|
||||
|
||||
progressBar = None
|
||||
if not opts.quiet:
|
||||
if opts.useProgressBar:
|
||||
try:
|
||||
tc = ProgressBar.TerminalController()
|
||||
progressBar = ProgressBar.ProgressBar(tc, header)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if not progressBar:
|
||||
print header
|
||||
|
||||
# Don't create more threads than tests.
|
||||
opts.numThreads = min(len(tests), opts.numThreads)
|
||||
|
||||
startTime = time.time()
|
||||
display = TestingProgressDisplay(opts, len(tests), progressBar)
|
||||
provider = TestProvider(cfg, opts, tests, display)
|
||||
runTests(opts, provider)
|
||||
display.finish()
|
||||
|
||||
if not opts.quiet:
|
||||
print 'Testing Time: %.2fs'%(time.time() - startTime)
|
||||
|
||||
# List test results organized by kind.
|
||||
byCode = {}
|
||||
for t in provider.results:
|
||||
if t:
|
||||
if t.code not in byCode:
|
||||
byCode[t.code] = []
|
||||
byCode[t.code].append(t)
|
||||
for title,code in (('Unexpected Passing Tests', TestStatus.XPass),
|
||||
('Failing Tests', TestStatus.Fail)):
|
||||
elts = byCode.get(code)
|
||||
if not elts:
|
||||
continue
|
||||
print '*'*20
|
||||
print '%s (%d):' % (title, len(elts))
|
||||
for tr in elts:
|
||||
print '\t%s'%(tr.path,)
|
||||
|
||||
numFailures = len(byCode.get(TestStatus.Fail,[]))
|
||||
if numFailures:
|
||||
print '\nFailures: %d' % (numFailures,)
|
||||
sys.exit(1)
|
||||
|
||||
if opts.timeTests:
|
||||
print '\nTest Times:'
|
||||
provider.results.sort(key=lambda t: t and t.elapsed)
|
||||
for tr in provider.results:
|
||||
if tr:
|
||||
print '%.2fs: %s' % (tr.elapsed, tr.path)
|
||||
|
||||
if __name__=='__main__':
|
||||
main()
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Source: http://code.activestate.com/recipes/475116/, with
|
||||
# modifications by Daniel Dunbar.
|
||||
|
||||
import sys, re, time
|
||||
|
||||
class TerminalController:
|
||||
"""
|
||||
A class that can be used to portably generate formatted output to
|
||||
a terminal.
|
||||
|
||||
`TerminalController` defines a set of instance variables whose
|
||||
values are initialized to the control sequence necessary to
|
||||
perform a given action. These can be simply included in normal
|
||||
output to the terminal:
|
||||
|
||||
>>> term = TerminalController()
|
||||
>>> print 'This is '+term.GREEN+'green'+term.NORMAL
|
||||
|
||||
Alternatively, the `render()` method can used, which replaces
|
||||
'${action}' with the string required to perform 'action':
|
||||
|
||||
>>> term = TerminalController()
|
||||
>>> print term.render('This is ${GREEN}green${NORMAL}')
|
||||
|
||||
If the terminal doesn't support a given action, then the value of
|
||||
the corresponding instance variable will be set to ''. As a
|
||||
result, the above code will still work on terminals that do not
|
||||
support color, except that their output will not be colored.
|
||||
Also, this means that you can test whether the terminal supports a
|
||||
given action by simply testing the truth value of the
|
||||
corresponding instance variable:
|
||||
|
||||
>>> term = TerminalController()
|
||||
>>> if term.CLEAR_SCREEN:
|
||||
... print 'This terminal supports clearning the screen.'
|
||||
|
||||
Finally, if the width and height of the terminal are known, then
|
||||
they will be stored in the `COLS` and `LINES` attributes.
|
||||
"""
|
||||
# Cursor movement:
|
||||
BOL = '' #: Move the cursor to the beginning of the line
|
||||
UP = '' #: Move the cursor up one line
|
||||
DOWN = '' #: Move the cursor down one line
|
||||
LEFT = '' #: Move the cursor left one char
|
||||
RIGHT = '' #: Move the cursor right one char
|
||||
|
||||
# Deletion:
|
||||
CLEAR_SCREEN = '' #: Clear the screen and move to home position
|
||||
CLEAR_EOL = '' #: Clear to the end of the line.
|
||||
CLEAR_BOL = '' #: Clear to the beginning of the line.
|
||||
CLEAR_EOS = '' #: Clear to the end of the screen
|
||||
|
||||
# Output modes:
|
||||
BOLD = '' #: Turn on bold mode
|
||||
BLINK = '' #: Turn on blink mode
|
||||
DIM = '' #: Turn on half-bright mode
|
||||
REVERSE = '' #: Turn on reverse-video mode
|
||||
NORMAL = '' #: Turn off all modes
|
||||
|
||||
# Cursor display:
|
||||
HIDE_CURSOR = '' #: Make the cursor invisible
|
||||
SHOW_CURSOR = '' #: Make the cursor visible
|
||||
|
||||
# Terminal size:
|
||||
COLS = None #: Width of the terminal (None for unknown)
|
||||
LINES = None #: Height of the terminal (None for unknown)
|
||||
|
||||
# Foreground colors:
|
||||
BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = ''
|
||||
|
||||
# Background colors:
|
||||
BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = ''
|
||||
BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = ''
|
||||
|
||||
_STRING_CAPABILITIES = """
|
||||
BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1
|
||||
CLEAR_SCREEN=clear CLEAR_EOL=el CLEAR_BOL=el1 CLEAR_EOS=ed BOLD=bold
|
||||
BLINK=blink DIM=dim REVERSE=rev UNDERLINE=smul NORMAL=sgr0
|
||||
HIDE_CURSOR=cinvis SHOW_CURSOR=cnorm""".split()
|
||||
_COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split()
|
||||
_ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split()
|
||||
|
||||
def __init__(self, term_stream=sys.stdout):
|
||||
"""
|
||||
Create a `TerminalController` and initialize its attributes
|
||||
with appropriate values for the current terminal.
|
||||
`term_stream` is the stream that will be used for terminal
|
||||
output; if this stream is not a tty, then the terminal is
|
||||
assumed to be a dumb terminal (i.e., have no capabilities).
|
||||
"""
|
||||
# Curses isn't available on all platforms
|
||||
try: import curses
|
||||
except: return
|
||||
|
||||
# If the stream isn't a tty, then assume it has no capabilities.
|
||||
if not term_stream.isatty(): return
|
||||
|
||||
# Check the terminal type. If we fail, then assume that the
|
||||
# terminal has no capabilities.
|
||||
try: curses.setupterm()
|
||||
except: return
|
||||
|
||||
# Look up numeric capabilities.
|
||||
self.COLS = curses.tigetnum('cols')
|
||||
self.LINES = curses.tigetnum('lines')
|
||||
|
||||
# Look up string capabilities.
|
||||
for capability in self._STRING_CAPABILITIES:
|
||||
(attrib, cap_name) = capability.split('=')
|
||||
setattr(self, attrib, self._tigetstr(cap_name) or '')
|
||||
|
||||
# Colors
|
||||
set_fg = self._tigetstr('setf')
|
||||
if set_fg:
|
||||
for i,color in zip(range(len(self._COLORS)), self._COLORS):
|
||||
setattr(self, color, curses.tparm(set_fg, i) or '')
|
||||
set_fg_ansi = self._tigetstr('setaf')
|
||||
if set_fg_ansi:
|
||||
for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
|
||||
setattr(self, color, curses.tparm(set_fg_ansi, i) or '')
|
||||
set_bg = self._tigetstr('setb')
|
||||
if set_bg:
|
||||
for i,color in zip(range(len(self._COLORS)), self._COLORS):
|
||||
setattr(self, 'BG_'+color, curses.tparm(set_bg, i) or '')
|
||||
set_bg_ansi = self._tigetstr('setab')
|
||||
if set_bg_ansi:
|
||||
for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
|
||||
setattr(self, 'BG_'+color, curses.tparm(set_bg_ansi, i) or '')
|
||||
|
||||
def _tigetstr(self, cap_name):
|
||||
# String capabilities can include "delays" of the form "$<2>".
|
||||
# For any modern terminal, we should be able to just ignore
|
||||
# these, so strip them out.
|
||||
import curses
|
||||
cap = curses.tigetstr(cap_name) or ''
|
||||
return re.sub(r'\$<\d+>[/*]?', '', cap)
|
||||
|
||||
def render(self, template):
|
||||
"""
|
||||
Replace each $-substitutions in the given template string with
|
||||
the corresponding terminal control string (if it's defined) or
|
||||
'' (if it's not).
|
||||
"""
|
||||
return re.sub(r'\$\$|\${\w+}', self._render_sub, template)
|
||||
|
||||
def _render_sub(self, match):
|
||||
s = match.group()
|
||||
if s == '$$': return s
|
||||
else: return getattr(self, s[2:-1])
|
||||
|
||||
#######################################################################
|
||||
# Example use case: progress bar
|
||||
#######################################################################
|
||||
|
||||
class ProgressBar:
|
||||
"""
|
||||
A 3-line progress bar, which looks like::
|
||||
|
||||
Header
|
||||
20% [===========----------------------------------]
|
||||
progress message
|
||||
|
||||
The progress bar is colored, if the terminal supports color
|
||||
output; and adjusts to the width of the terminal.
|
||||
"""
|
||||
BAR = '%s${GREEN}[${BOLD}%s%s${NORMAL}${GREEN}]${NORMAL}%s\n'
|
||||
HEADER = '${BOLD}${CYAN}%s${NORMAL}\n\n'
|
||||
|
||||
def __init__(self, term, header, useETA=True):
|
||||
self.term = term
|
||||
if not (self.term.CLEAR_EOL and self.term.UP and self.term.BOL):
|
||||
raise ValueError("Terminal isn't capable enough -- you "
|
||||
"should use a simpler progress dispaly.")
|
||||
self.width = self.term.COLS or 75
|
||||
self.bar = term.render(self.BAR)
|
||||
self.header = self.term.render(self.HEADER % header.center(self.width))
|
||||
self.cleared = 1 #: true if we haven't drawn the bar yet.
|
||||
self.useETA = useETA
|
||||
if self.useETA:
|
||||
self.startTime = time.time()
|
||||
self.update(0, '')
|
||||
|
||||
def update(self, percent, message):
|
||||
if self.cleared:
|
||||
sys.stdout.write(self.header)
|
||||
self.cleared = 0
|
||||
prefix = '%3d%% ' % (percent*100,)
|
||||
suffix = ''
|
||||
if self.useETA:
|
||||
elapsed = time.time() - self.startTime
|
||||
if percent > .0001 and elapsed > 1:
|
||||
total = elapsed / percent
|
||||
eta = int(total - elapsed)
|
||||
h = eta//3600.
|
||||
m = (eta//60) % 60
|
||||
s = eta % 60
|
||||
suffix = ' ETA: %02d:%02d:%02d'%(h,m,s)
|
||||
barWidth = self.width - len(prefix) - len(suffix) - 2
|
||||
n = int(barWidth*percent)
|
||||
if len(message) < self.width:
|
||||
message = message + ' '*(self.width - len(message))
|
||||
else:
|
||||
message = '... ' + message[-(self.width-4):]
|
||||
sys.stdout.write(
|
||||
self.term.BOL + self.term.UP + self.term.CLEAR_EOL +
|
||||
(self.bar % (prefix, '='*n, '-'*(barWidth-n), suffix)) +
|
||||
self.term.CLEAR_EOL + message)
|
||||
|
||||
def clear(self):
|
||||
if not self.cleared:
|
||||
sys.stdout.write(self.term.BOL + self.term.CLEAR_EOL +
|
||||
self.term.UP + self.term.CLEAR_EOL +
|
||||
self.term.UP + self.term.CLEAR_EOL)
|
||||
self.cleared = 1
|
||||
|
||||
def test():
|
||||
import time
|
||||
tc = TerminalController()
|
||||
p = ProgressBar(tc, 'Tests')
|
||||
for i in range(101):
|
||||
p.update(i/100., str(i))
|
||||
time.sleep(.3)
|
||||
|
||||
if __name__=='__main__':
|
||||
test()
|
||||
|
|
@ -1,390 +0,0 @@
|
|||
import itertools
|
||||
|
||||
import Util
|
||||
|
||||
class ShLexer:
|
||||
def __init__(self, data, win32Escapes = False):
|
||||
self.data = data
|
||||
self.pos = 0
|
||||
self.end = len(data)
|
||||
self.win32Escapes = win32Escapes
|
||||
|
||||
def eat(self):
|
||||
c = self.data[self.pos]
|
||||
self.pos += 1
|
||||
return c
|
||||
|
||||
def look(self):
|
||||
return self.data[self.pos]
|
||||
|
||||
def maybe_eat(self, c):
|
||||
"""
|
||||
maybe_eat(c) - Consume the character c if it is the next character,
|
||||
returning True if a character was consumed. """
|
||||
if self.data[self.pos] == c:
|
||||
self.pos += 1
|
||||
return True
|
||||
return False
|
||||
|
||||
def lex_arg_fast(self, c):
|
||||
# Get the leading whitespace free section.
|
||||
chunk = self.data[self.pos - 1:].split(None, 1)[0]
|
||||
|
||||
# If it has special characters, the fast path failed.
|
||||
if ('|' in chunk or '&' in chunk or
|
||||
'<' in chunk or '>' in chunk or
|
||||
"'" in chunk or '"' in chunk or
|
||||
'\\' in chunk):
|
||||
return None
|
||||
|
||||
self.pos = self.pos - 1 + len(chunk)
|
||||
return chunk
|
||||
|
||||
def lex_arg_slow(self, c):
|
||||
if c in "'\"":
|
||||
str = self.lex_arg_quoted(c)
|
||||
else:
|
||||
str = c
|
||||
while self.pos != self.end:
|
||||
c = self.look()
|
||||
if c.isspace() or c in "|&":
|
||||
break
|
||||
elif c in '><':
|
||||
# This is an annoying case; we treat '2>' as a single token so
|
||||
# we don't have to track whitespace tokens.
|
||||
|
||||
# If the parse string isn't an integer, do the usual thing.
|
||||
if not str.isdigit():
|
||||
break
|
||||
|
||||
# Otherwise, lex the operator and convert to a redirection
|
||||
# token.
|
||||
num = int(str)
|
||||
tok = self.lex_one_token()
|
||||
assert isinstance(tok, tuple) and len(tok) == 1
|
||||
return (tok[0], num)
|
||||
elif c == '"':
|
||||
self.eat()
|
||||
str += self.lex_arg_quoted('"')
|
||||
elif not self.win32Escapes and c == '\\':
|
||||
# Outside of a string, '\\' escapes everything.
|
||||
self.eat()
|
||||
if self.pos == self.end:
|
||||
Util.warning("escape at end of quoted argument in: %r" %
|
||||
self.data)
|
||||
return str
|
||||
str += self.eat()
|
||||
else:
|
||||
str += self.eat()
|
||||
return str
|
||||
|
||||
def lex_arg_quoted(self, delim):
|
||||
str = ''
|
||||
while self.pos != self.end:
|
||||
c = self.eat()
|
||||
if c == delim:
|
||||
return str
|
||||
elif c == '\\' and delim == '"':
|
||||
# Inside a '"' quoted string, '\\' only escapes the quote
|
||||
# character and backslash, otherwise it is preserved.
|
||||
if self.pos == self.end:
|
||||
Util.warning("escape at end of quoted argument in: %r" %
|
||||
self.data)
|
||||
return str
|
||||
c = self.eat()
|
||||
if c == '"': #
|
||||
str += '"'
|
||||
elif c == '\\':
|
||||
str += '\\'
|
||||
else:
|
||||
str += '\\' + c
|
||||
else:
|
||||
str += c
|
||||
Util.warning("missing quote character in %r" % self.data)
|
||||
return str
|
||||
|
||||
def lex_arg_checked(self, c):
|
||||
pos = self.pos
|
||||
res = self.lex_arg_fast(c)
|
||||
end = self.pos
|
||||
|
||||
self.pos = pos
|
||||
reference = self.lex_arg_slow(c)
|
||||
if res is not None:
|
||||
if res != reference:
|
||||
raise ValueError,"Fast path failure: %r != %r" % (res, reference)
|
||||
if self.pos != end:
|
||||
raise ValueError,"Fast path failure: %r != %r" % (self.pos, end)
|
||||
return reference
|
||||
|
||||
def lex_arg(self, c):
|
||||
return self.lex_arg_fast(c) or self.lex_arg_slow(c)
|
||||
|
||||
def lex_one_token(self):
|
||||
"""
|
||||
lex_one_token - Lex a single 'sh' token. """
|
||||
|
||||
c = self.eat()
|
||||
if c in ';!':
|
||||
return (c,)
|
||||
if c == '|':
|
||||
if self.maybe_eat('|'):
|
||||
return ('||',)
|
||||
return (c,)
|
||||
if c == '&':
|
||||
if self.maybe_eat('&'):
|
||||
return ('&&',)
|
||||
if self.maybe_eat('>'):
|
||||
return ('&>',)
|
||||
return (c,)
|
||||
if c == '>':
|
||||
if self.maybe_eat('&'):
|
||||
return ('>&',)
|
||||
if self.maybe_eat('>'):
|
||||
return ('>>',)
|
||||
return (c,)
|
||||
if c == '<':
|
||||
if self.maybe_eat('&'):
|
||||
return ('<&',)
|
||||
if self.maybe_eat('>'):
|
||||
return ('<<',)
|
||||
return (c,)
|
||||
|
||||
return self.lex_arg(c)
|
||||
|
||||
def lex(self):
|
||||
while self.pos != self.end:
|
||||
if self.look().isspace():
|
||||
self.eat()
|
||||
else:
|
||||
yield self.lex_one_token()
|
||||
|
||||
###
|
||||
|
||||
class Command:
|
||||
def __init__(self, args, redirects):
|
||||
self.args = list(args)
|
||||
self.redirects = list(redirects)
|
||||
|
||||
def __repr__(self):
|
||||
return 'Command(%r, %r)' % (self.args, self.redirects)
|
||||
|
||||
def __cmp__(self, other):
|
||||
if not isinstance(other, Command):
|
||||
return -1
|
||||
|
||||
return cmp((self.args, self.redirects),
|
||||
(other.args, other.redirects))
|
||||
|
||||
class Pipeline:
|
||||
def __init__(self, commands, negate):
|
||||
self.commands = commands
|
||||
self.negate = negate
|
||||
|
||||
def __repr__(self):
|
||||
return 'Pipeline(%r, %r)' % (self.commands, self.negate)
|
||||
|
||||
def __cmp__(self, other):
|
||||
if not isinstance(other, Pipeline):
|
||||
return -1
|
||||
|
||||
return cmp((self.commands, self.negate),
|
||||
(other.commands, other.negate))
|
||||
|
||||
class Seq:
|
||||
def __init__(self, lhs, op, rhs):
|
||||
assert op in (';', '&', '||', '&&')
|
||||
self.op = op
|
||||
self.lhs = lhs
|
||||
self.rhs = rhs
|
||||
|
||||
def __repr__(self):
|
||||
return 'Seq(%r, %r, %r)' % (self.lhs, self.op, self.rhs)
|
||||
|
||||
def __cmp__(self, other):
|
||||
if not isinstance(other, Seq):
|
||||
return -1
|
||||
|
||||
return cmp((self.lhs, self.op, self.rhs),
|
||||
(other.lhs, other.op, other.rhs))
|
||||
|
||||
class ShParser:
|
||||
def __init__(self, data, win32Escapes = False):
|
||||
self.data = data
|
||||
self.tokens = ShLexer(data, win32Escapes = win32Escapes).lex()
|
||||
|
||||
def lex(self):
|
||||
try:
|
||||
return self.tokens.next()
|
||||
except StopIteration:
|
||||
return None
|
||||
|
||||
def look(self):
|
||||
next = self.lex()
|
||||
if next is not None:
|
||||
self.tokens = itertools.chain([next], self.tokens)
|
||||
return next
|
||||
|
||||
def parse_command(self):
|
||||
tok = self.lex()
|
||||
if not tok:
|
||||
raise ValueError,"empty command!"
|
||||
if isinstance(tok, tuple):
|
||||
raise ValueError,"syntax error near unexpected token %r" % tok[0]
|
||||
|
||||
args = [tok]
|
||||
redirects = []
|
||||
while 1:
|
||||
tok = self.look()
|
||||
|
||||
# EOF?
|
||||
if tok is None:
|
||||
break
|
||||
|
||||
# If this is an argument, just add it to the current command.
|
||||
if isinstance(tok, str):
|
||||
args.append(self.lex())
|
||||
continue
|
||||
|
||||
# Otherwise see if it is a terminator.
|
||||
assert isinstance(tok, tuple)
|
||||
if tok[0] in ('|',';','&','||','&&'):
|
||||
break
|
||||
|
||||
# Otherwise it must be a redirection.
|
||||
op = self.lex()
|
||||
arg = self.lex()
|
||||
if not arg:
|
||||
raise ValueError,"syntax error near token %r" % op[0]
|
||||
redirects.append((op, arg))
|
||||
|
||||
return Command(args, redirects)
|
||||
|
||||
def parse_pipeline(self):
|
||||
negate = False
|
||||
if self.look() == ('!',):
|
||||
self.lex()
|
||||
negate = True
|
||||
|
||||
commands = [self.parse_command()]
|
||||
while self.look() == ('|',):
|
||||
self.lex()
|
||||
commands.append(self.parse_command())
|
||||
return Pipeline(commands, negate)
|
||||
|
||||
def parse(self):
|
||||
lhs = self.parse_pipeline()
|
||||
|
||||
while self.look():
|
||||
operator = self.lex()
|
||||
assert isinstance(operator, tuple) and len(operator) == 1
|
||||
|
||||
if not self.look():
|
||||
raise ValueError, "missing argument to operator %r" % operator[0]
|
||||
|
||||
# FIXME: Operator precedence!!
|
||||
lhs = Seq(lhs, operator[0], self.parse_pipeline())
|
||||
|
||||
return lhs
|
||||
|
||||
###
|
||||
|
||||
import unittest
|
||||
|
||||
class TestShLexer(unittest.TestCase):
|
||||
def lex(self, str, *args, **kwargs):
|
||||
return list(ShLexer(str, *args, **kwargs).lex())
|
||||
|
||||
def test_basic(self):
|
||||
self.assertEqual(self.lex('a|b>c&d<e'),
|
||||
['a', ('|',), 'b', ('>',), 'c', ('&',), 'd',
|
||||
('<',), 'e'])
|
||||
|
||||
def test_redirection_tokens(self):
|
||||
self.assertEqual(self.lex('a2>c'),
|
||||
['a2', ('>',), 'c'])
|
||||
self.assertEqual(self.lex('a 2>c'),
|
||||
['a', ('>',2), 'c'])
|
||||
|
||||
def test_quoting(self):
|
||||
self.assertEqual(self.lex(""" 'a' """),
|
||||
['a'])
|
||||
self.assertEqual(self.lex(""" "hello\\"world" """),
|
||||
['hello"world'])
|
||||
self.assertEqual(self.lex(""" "hello\\'world" """),
|
||||
["hello\\'world"])
|
||||
self.assertEqual(self.lex(""" "hello\\\\world" """),
|
||||
["hello\\world"])
|
||||
self.assertEqual(self.lex(""" he"llo wo"rld """),
|
||||
["hello world"])
|
||||
self.assertEqual(self.lex(""" a\\ b a\\\\b """),
|
||||
["a b", "a\\b"])
|
||||
self.assertEqual(self.lex(""" "" "" """),
|
||||
["", ""])
|
||||
self.assertEqual(self.lex(""" a\\ b """, win32Escapes = True),
|
||||
['a\\', 'b'])
|
||||
|
||||
class TestShParse(unittest.TestCase):
|
||||
def parse(self, str):
|
||||
return ShParser(str).parse()
|
||||
|
||||
def test_basic(self):
|
||||
self.assertEqual(self.parse('echo hello'),
|
||||
Pipeline([Command(['echo', 'hello'], [])], False))
|
||||
self.assertEqual(self.parse('echo ""'),
|
||||
Pipeline([Command(['echo', ''], [])], False))
|
||||
|
||||
def test_redirection(self):
|
||||
self.assertEqual(self.parse('echo hello > c'),
|
||||
Pipeline([Command(['echo', 'hello'],
|
||||
[((('>'),), 'c')])], False))
|
||||
self.assertEqual(self.parse('echo hello > c >> d'),
|
||||
Pipeline([Command(['echo', 'hello'], [(('>',), 'c'),
|
||||
(('>>',), 'd')])], False))
|
||||
|
||||
def test_pipeline(self):
|
||||
self.assertEqual(self.parse('a | b'),
|
||||
Pipeline([Command(['a'], []),
|
||||
Command(['b'], [])],
|
||||
False))
|
||||
|
||||
self.assertEqual(self.parse('a | b | c'),
|
||||
Pipeline([Command(['a'], []),
|
||||
Command(['b'], []),
|
||||
Command(['c'], [])],
|
||||
False))
|
||||
|
||||
self.assertEqual(self.parse('! a'),
|
||||
Pipeline([Command(['a'], [])],
|
||||
True))
|
||||
|
||||
def test_list(self):
|
||||
self.assertEqual(self.parse('a ; b'),
|
||||
Seq(Pipeline([Command(['a'], [])], False),
|
||||
';',
|
||||
Pipeline([Command(['b'], [])], False)))
|
||||
|
||||
self.assertEqual(self.parse('a & b'),
|
||||
Seq(Pipeline([Command(['a'], [])], False),
|
||||
'&',
|
||||
Pipeline([Command(['b'], [])], False)))
|
||||
|
||||
self.assertEqual(self.parse('a && b'),
|
||||
Seq(Pipeline([Command(['a'], [])], False),
|
||||
'&&',
|
||||
Pipeline([Command(['b'], [])], False)))
|
||||
|
||||
self.assertEqual(self.parse('a || b'),
|
||||
Seq(Pipeline([Command(['a'], [])], False),
|
||||
'||',
|
||||
Pipeline([Command(['b'], [])], False)))
|
||||
|
||||
self.assertEqual(self.parse('a && b || c'),
|
||||
Seq(Seq(Pipeline([Command(['a'], [])], False),
|
||||
'&&',
|
||||
Pipeline([Command(['b'], [])], False)),
|
||||
'||',
|
||||
Pipeline([Command(['c'], [])], False)))
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
@ -1,334 +0,0 @@
|
|||
import os
|
||||
import platform
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import ShUtil
|
||||
import Util
|
||||
|
||||
kSystemName = platform.system()
|
||||
|
||||
class TestStatus:
|
||||
Pass = 0
|
||||
XFail = 1
|
||||
Fail = 2
|
||||
XPass = 3
|
||||
Invalid = 4
|
||||
|
||||
kNames = ['Pass','XFail','Fail','XPass','Invalid']
|
||||
@staticmethod
|
||||
def getName(code):
|
||||
return TestStatus.kNames[code]
|
||||
|
||||
def executeShCmd(cmd, cfg, cwd, results):
|
||||
if isinstance(cmd, ShUtil.Seq):
|
||||
if cmd.op == ';':
|
||||
res = executeShCmd(cmd.lhs, cfg, cwd, results)
|
||||
return executeShCmd(cmd.rhs, cfg, cwd, results)
|
||||
|
||||
if cmd.op == '&':
|
||||
raise NotImplementedError,"unsupported test command: '&'"
|
||||
|
||||
if cmd.op == '||':
|
||||
res = executeShCmd(cmd.lhs, cfg, cwd, results)
|
||||
if res != 0:
|
||||
res = executeShCmd(cmd.rhs, cfg, cwd, results)
|
||||
return res
|
||||
if cmd.op == '&&':
|
||||
res = executeShCmd(cmd.lhs, cfg, cwd, results)
|
||||
if res is None:
|
||||
return res
|
||||
|
||||
if res == 0:
|
||||
res = executeShCmd(cmd.rhs, cfg, cwd, results)
|
||||
return res
|
||||
|
||||
raise ValueError,'Unknown shell command: %r' % cmd.op
|
||||
|
||||
assert isinstance(cmd, ShUtil.Pipeline)
|
||||
procs = []
|
||||
input = subprocess.PIPE
|
||||
for j in cmd.commands:
|
||||
# FIXME: This is broken, it doesn't account for the accumulative nature
|
||||
# of redirects.
|
||||
stdin = input
|
||||
stdout = stderr = subprocess.PIPE
|
||||
for r in j.redirects:
|
||||
if r[0] == ('>',2):
|
||||
stderr = open(r[1], 'w')
|
||||
elif r[0] == ('>&',2) and r[1] == '1':
|
||||
stderr = subprocess.STDOUT
|
||||
elif r[0] == ('>',):
|
||||
stdout = open(r[1], 'w')
|
||||
elif r[0] == ('<',):
|
||||
stdin = open(r[1], 'r')
|
||||
else:
|
||||
raise NotImplementedError,"Unsupported redirect: %r" % r
|
||||
|
||||
procs.append(subprocess.Popen(j.args, cwd=cwd,
|
||||
stdin = stdin,
|
||||
stdout = stdout,
|
||||
stderr = stderr,
|
||||
env = cfg.environment))
|
||||
|
||||
# Immediately close stdin for any process taking stdin from us.
|
||||
if stdin == subprocess.PIPE:
|
||||
procs[-1].stdin.close()
|
||||
procs[-1].stdin = None
|
||||
|
||||
if stdout == subprocess.PIPE:
|
||||
input = procs[-1].stdout
|
||||
else:
|
||||
input = subprocess.PIPE
|
||||
|
||||
# FIXME: There is a potential for deadlock here, when we have a pipe and
|
||||
# some process other than the last one ends up blocked on stderr.
|
||||
procData = [None] * len(procs)
|
||||
procData[-1] = procs[-1].communicate()
|
||||
for i in range(len(procs) - 1):
|
||||
if procs[i].stdout is not None:
|
||||
out = procs[i].stdout.read()
|
||||
else:
|
||||
out = ''
|
||||
if procs[i].stderr is not None:
|
||||
err = procs[i].stderr.read()
|
||||
else:
|
||||
err = ''
|
||||
procData[i] = (out,err)
|
||||
|
||||
# FIXME: Fix tests to work with pipefail, and make exitCode max across
|
||||
# procs.
|
||||
for i,(out,err) in enumerate(procData):
|
||||
exitCode = res = procs[i].wait()
|
||||
results.append((cmd.commands[i], out, err, res))
|
||||
|
||||
if cmd.negate:
|
||||
exitCode = not exitCode
|
||||
|
||||
return exitCode
|
||||
|
||||
def executeScriptInternal(cfg, commands, cwd):
|
||||
cmd = ShUtil.ShParser(' &&\n'.join(commands),
|
||||
kSystemName == 'Windows').parse()
|
||||
|
||||
results = []
|
||||
try:
|
||||
exitCode = executeShCmd(cmd, cfg, cwd, results)
|
||||
except:
|
||||
import traceback
|
||||
|
||||
out = ''
|
||||
err = 'Exception during script execution:\n%s\n' % traceback.format_exc()
|
||||
return out, err, 127
|
||||
|
||||
out = err = ''
|
||||
for i,(cmd, cmd_out,cmd_err,res) in enumerate(results):
|
||||
out += 'Command %d: %s\n' % (i, ' '.join('"%s"' % s for s in cmd.args))
|
||||
out += 'Command %d Result: %r\n' % (i, res)
|
||||
out += 'Command %d Output:\n%s\n\n' % (i, cmd_out)
|
||||
out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err)
|
||||
|
||||
return out, err, exitCode
|
||||
|
||||
def executeScript(cfg, script, commands, cwd):
|
||||
# Write script file
|
||||
f = open(script,'w')
|
||||
if kSystemName == 'Windows':
|
||||
f.write('\nif %ERRORLEVEL% NEQ 0 EXIT\n'.join(commands))
|
||||
else:
|
||||
f.write(' &&\n'.join(commands))
|
||||
f.write('\n')
|
||||
f.close()
|
||||
|
||||
if kSystemName == 'Windows':
|
||||
command = ['cmd','/c', script]
|
||||
else:
|
||||
command = ['/bin/sh', script]
|
||||
if cfg.useValgrind:
|
||||
# FIXME: Running valgrind on sh is overkill. We probably could just
|
||||
# run on clang with no real loss.
|
||||
valgrindArgs = ['valgrind', '-q',
|
||||
'--tool=memcheck', '--trace-children=yes',
|
||||
'--error-exitcode=123'] + cfg.valgrindArgs
|
||||
command = valgrindArgs + command
|
||||
|
||||
p = subprocess.Popen(command, cwd=cwd,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
env=cfg.environment)
|
||||
out,err = p.communicate()
|
||||
exitCode = p.wait()
|
||||
|
||||
return out, err, exitCode
|
||||
|
||||
import StringIO
|
||||
def runOneTest(cfg, testPath, tmpBase):
|
||||
# Make paths absolute.
|
||||
tmpBase = os.path.abspath(tmpBase)
|
||||
testPath = os.path.abspath(testPath)
|
||||
|
||||
# Create the output directory if it does not already exist.
|
||||
|
||||
Util.mkdir_p(os.path.dirname(tmpBase))
|
||||
script = tmpBase + '.script'
|
||||
if kSystemName == 'Windows':
|
||||
script += '.bat'
|
||||
|
||||
substitutions = [('%s', testPath),
|
||||
('%S', os.path.dirname(testPath)),
|
||||
('%t', tmpBase + '.tmp'),
|
||||
(' clang ', ' ' + cfg.clang + ' '),
|
||||
(' clang-cc ', ' ' + cfg.clangcc + ' ')]
|
||||
|
||||
# Collect the test lines from the script.
|
||||
scriptLines = []
|
||||
xfailLines = []
|
||||
for ln in open(testPath):
|
||||
if 'RUN:' in ln:
|
||||
# Isolate the command to run.
|
||||
index = ln.index('RUN:')
|
||||
ln = ln[index+4:]
|
||||
|
||||
# Strip trailing newline.
|
||||
scriptLines.append(ln)
|
||||
elif 'XFAIL' in ln:
|
||||
xfailLines.append(ln)
|
||||
|
||||
# FIXME: Support something like END, in case we need to process large
|
||||
# files.
|
||||
|
||||
# Verify the script contains a run line.
|
||||
if not scriptLines:
|
||||
return (TestStatus.Fail, "Test has no run line!")
|
||||
|
||||
# Apply substitutions to the script.
|
||||
def processLine(ln):
|
||||
# Apply substitutions
|
||||
for a,b in substitutions:
|
||||
ln = ln.replace(a,b)
|
||||
|
||||
# Strip the trailing newline and any extra whitespace.
|
||||
return ln.strip()
|
||||
scriptLines = map(processLine, scriptLines)
|
||||
|
||||
# Validate interior lines for '&&', a lovely historical artifact.
|
||||
for i in range(len(scriptLines) - 1):
|
||||
ln = scriptLines[i]
|
||||
|
||||
if not ln.endswith('&&'):
|
||||
return (TestStatus.Fail,
|
||||
("MISSING \'&&\': %s\n" +
|
||||
"FOLLOWED BY : %s\n") % (ln, scriptLines[i + 1]))
|
||||
|
||||
# Strip off '&&'
|
||||
scriptLines[i] = ln[:-2]
|
||||
|
||||
if not cfg.useExternalShell:
|
||||
res = executeScriptInternal(cfg, scriptLines, os.path.dirname(testPath))
|
||||
|
||||
if res is not None:
|
||||
out, err, exitCode = res
|
||||
elif True:
|
||||
return (TestStatus.Fail,
|
||||
"Unable to execute internally:\n%s\n"
|
||||
% '\n'.join(scriptLines))
|
||||
else:
|
||||
out, err, exitCode = executeScript(cfg, script, scriptLines,
|
||||
os.path.dirname(testPath))
|
||||
else:
|
||||
out, err, exitCode = executeScript(cfg, script, scriptLines,
|
||||
os.path.dirname(testPath))
|
||||
|
||||
# Detect Ctrl-C in subprocess.
|
||||
if exitCode == -signal.SIGINT:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
if xfailLines:
|
||||
ok = exitCode != 0
|
||||
status = (TestStatus.XPass, TestStatus.XFail)[ok]
|
||||
else:
|
||||
ok = exitCode == 0
|
||||
status = (TestStatus.Fail, TestStatus.Pass)[ok]
|
||||
|
||||
if ok:
|
||||
return (status,'')
|
||||
|
||||
output = StringIO.StringIO()
|
||||
print >>output, "Script:"
|
||||
print >>output, "--"
|
||||
print >>output, '\n'.join(scriptLines)
|
||||
print >>output, "--"
|
||||
print >>output, "Exit Code: %r" % exitCode
|
||||
print >>output, "Command Output (stdout):"
|
||||
print >>output, "--"
|
||||
output.write(out)
|
||||
print >>output, "--"
|
||||
print >>output, "Command Output (stderr):"
|
||||
print >>output, "--"
|
||||
output.write(err)
|
||||
print >>output, "--"
|
||||
return (status, output.getvalue())
|
||||
|
||||
def capture(args):
|
||||
p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out,_ = p.communicate()
|
||||
return out
|
||||
|
||||
def inferClang(cfg):
|
||||
# Determine which clang to use.
|
||||
clang = os.getenv('CLANG')
|
||||
|
||||
# If the user set clang in the environment, definitely use that and don't
|
||||
# try to validate.
|
||||
if clang:
|
||||
return clang
|
||||
|
||||
# Otherwise look in the path.
|
||||
clang = Util.which('clang', cfg.environment['PATH'])
|
||||
|
||||
if not clang:
|
||||
print >>sys.stderr, "error: couldn't find 'clang' program, try setting CLANG in your environment"
|
||||
sys.exit(1)
|
||||
|
||||
return clang
|
||||
|
||||
def inferClangCC(cfg, clang):
|
||||
clangcc = os.getenv('CLANGCC')
|
||||
|
||||
# If the user set clang in the environment, definitely use that and don't
|
||||
# try to validate.
|
||||
if clangcc:
|
||||
return clangcc
|
||||
|
||||
# Otherwise try adding -cc since we expect to be looking in a build
|
||||
# directory.
|
||||
if clang.endswith('.exe'):
|
||||
clangccName = clang[:-4] + '-cc.exe'
|
||||
else:
|
||||
clangccName = clang + '-cc'
|
||||
clangcc = Util.which(clangccName, cfg.environment['PATH'])
|
||||
if not clangcc:
|
||||
# Otherwise ask clang.
|
||||
res = capture([clang, '-print-prog-name=clang-cc'])
|
||||
res = res.strip()
|
||||
if res and os.path.exists(res):
|
||||
clangcc = res
|
||||
|
||||
if not clangcc:
|
||||
print >>sys.stderr, "error: couldn't find 'clang-cc' program, try setting CLANGCC in your environment"
|
||||
sys.exit(1)
|
||||
|
||||
return clangcc
|
||||
|
||||
def getTestOutputBase(dir, testpath):
|
||||
"""getTestOutputBase(dir, testpath) - Get the full path for temporary files
|
||||
corresponding to the given test path."""
|
||||
|
||||
# Form the output base out of the test parent directory name and the test
|
||||
# name. FIXME: Find a better way to organize test results.
|
||||
return os.path.join(dir,
|
||||
os.path.basename(os.path.dirname(testpath)),
|
||||
os.path.basename(testpath))
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
class TestingConfig:
|
||||
""""
|
||||
TestingConfig - Information on a how to run a group of tests.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def frompath(path):
|
||||
data = {}
|
||||
f = open(path)
|
||||
exec f in {},data
|
||||
|
||||
return TestingConfig(suffixes = data.get('suffixes', []),
|
||||
environment = data.get('environment', {}))
|
||||
|
||||
def __init__(self, suffixes, environment):
|
||||
self.suffixes = set(suffixes)
|
||||
self.environment = dict(environment)
|
||||
|
||||
# Variables set internally.
|
||||
self.root = None
|
||||
self.useValgrind = None
|
||||
self.useExternalShell = None
|
||||
self.valgrindArgs = []
|
||||
|
||||
# FIXME: These need to move into a substitutions mechanism.
|
||||
self.clang = None
|
||||
self.clangcc = None
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
import errno, os, sys
|
||||
|
||||
def warning(msg):
|
||||
print >>sys.stderr, '%s: warning: %s' % (sys.argv[0], msg)
|
||||
|
||||
def detectCPUs():
|
||||
"""
|
||||
Detects the number of CPUs on a system. Cribbed from pp.
|
||||
"""
|
||||
# Linux, Unix and MacOS:
|
||||
if hasattr(os, "sysconf"):
|
||||
if os.sysconf_names.has_key("SC_NPROCESSORS_ONLN"):
|
||||
# Linux & Unix:
|
||||
ncpus = os.sysconf("SC_NPROCESSORS_ONLN")
|
||||
if isinstance(ncpus, int) and ncpus > 0:
|
||||
return ncpus
|
||||
else: # OSX:
|
||||
return int(os.popen2("sysctl -n hw.ncpu")[1].read())
|
||||
# Windows:
|
||||
if os.environ.has_key("NUMBER_OF_PROCESSORS"):
|
||||
ncpus = int(os.environ["NUMBER_OF_PROCESSORS"]);
|
||||
if ncpus > 0:
|
||||
return ncpus
|
||||
return 1 # Default
|
||||
|
||||
def mkdir_p(path):
|
||||
"""mkdir_p(path) - Make the "path" directory, if it does not exist; this
|
||||
will also make directories for any missing parent directories."""
|
||||
|
||||
if not path or os.path.exists(path):
|
||||
return
|
||||
|
||||
parent = os.path.dirname(path)
|
||||
if parent != path:
|
||||
mkdir_p(parent)
|
||||
|
||||
try:
|
||||
os.mkdir(path)
|
||||
except OSError,e:
|
||||
# Ignore EEXIST, which may occur during a race condition.
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
|
||||
def which(command, paths = None):
|
||||
"""which(command, [paths]) - Look up the given command in the paths string (or
|
||||
the PATH environment variable, if unspecified)."""
|
||||
|
||||
if paths is None:
|
||||
paths = os.environ.get('PATH','')
|
||||
|
||||
# Check for absolute match first.
|
||||
if os.path.exists(command):
|
||||
return command
|
||||
|
||||
# Would be nice if Python had a lib function for this.
|
||||
if not paths:
|
||||
paths = os.defpath
|
||||
|
||||
# Get suffixes to search.
|
||||
pathext = os.environ.get('PATHEXT', '').split(os.pathsep)
|
||||
|
||||
# Search the paths...
|
||||
for path in paths.split(os.pathsep):
|
||||
for ext in pathext:
|
||||
p = os.path.join(path, command + ext)
|
||||
if os.path.exists(p):
|
||||
return p
|
||||
|
||||
return None
|
||||
|
||||
Loading…
Reference in New Issue