#!/usr/bin/env python # # Copyright (c) 2015, 2016, 2017 Tim Savannah under following terms: # You may modify and redistribe this script with your project # # It will download the latest GoodTests.py and use it to execute the tests. # # This should be placed in a directory, "tests", at the root of your project. It assumes that ../$MY_PACKAGE_MODULE is the path to your test module, and will create a symlink to it in order to run tests. # The tests should be found in $MY_TEST_DIRECTORY in given "tests" folder. # NOTE: Since version 1.2.3, you can also import this (like from a graphical application) and call the "main()" function. # All of the following globals are the defaults, but can be overridden when calling main() (params have the same name as the globals). # Assign a local function, "find_mod" to the interface to search # PYTHONPATH for importable module try: # imp.find_module has been deprecated as of python 3.7, so # prefer some alternate/newer interfaces first. import importlib try: # If we have the newest and therefore least-deprecated # way, use it. _findModSpec = importlib.util.find_spec def find_mod(modName): ''' find_mod - Find a module by name. Similar to import #modName but only finds importable module, does not actually import. @raises ImportError on failure ''' modSpec = _findModSpec(modName) if not modSpec: # imp.find_module raises import error if cannot find, # but find_spec just returns None # So simulate the ImportError for common interface raise ImportError('No module named %s' %(modName, )) return modSpec except AttributeError: # We have importlib, but don't have importlib.util.find_spec # We could use importlib.import_module which is present in # python 2.7, but that changes behaviour by actually # importing (and thus additionally checking syntax/other). # # So just fall back to the old imp.find_module in this case try: # Clean up namespace del importlib except: pass # Fall back to imp.find_module implementation below raise ImportError('importlib but no importlib.util') #find_mod = lambda modName : importlib.import_module(modName) except: # importlib is not present or has an unknown/dated interface, # so fallback to the deprecated but oldest form import imp # Use a lambda to ensure only one arg is passed as that is # our standard interface find_mod = lambda modName : imp.find_module(modName) import os import subprocess import sys # URL to current version of GoodTests.py - You only need to change this if you host an internal copy. GOODTESTS_URL = 'https://raw.githubusercontent.com/kata198/GoodTests/master/GoodTests.py' # This should be your module name, and can be any relative or absolute path, or just a module name. # If just a module name is given, the directory must be in current directory or parent directory. MY_PACKAGE_MODULE = 'func_timeout' # Normally, you want to test the codebase during development, so you don't care about the site-packages installed version. # If you want to allow testing with any module by @MY_PACKAGE_MODULE in the python path, change this to True. ALLOW_SITE_INSTALL = False # This is the test directory that should contain all your tests. This should be a directory in your "tests" folder MY_TEST_DIRECTORY = 'FuncTimeoutTests' __version__ = '3.0.5' __version_tuple__ = (3, 0, 5) def findGoodTests(): ''' findGoodTests - Tries to find GoodTests.py @return { 'path' -> Path to GoodTests.py (for execution) 'success' -> True/False if we successfully found GoodTests.py } ''' pathSplit = os.environ['PATH'].split(':') if '.' not in pathSplit: pathSplit = ['.'] + pathSplit os.environ['PATH'] = ':'.join(pathSplit) result = '' success = False for path in pathSplit: if path.endswith('/'): path = path[:-1] guess = path + '/GoodTests.py' if os.path.exists(guess): success = True result = guess break return { 'path' : result, "success" : success } def findExecutable(execName): ''' findExecutable - Search PATH for an executable @return { 'path' -> Path to executable (if found, see "success") 'success' -> True/False if we successfully found requested executable } ''' pathSplit = os.environ['PATH'].split(':') if '.' not in pathSplit: pathSplit = ['.'] + pathSplit os.environ['PATH'] = ':'.join(pathSplit) result = '' success = False for path in pathSplit: if path.endswith(os.sep): path = path[:-1] guess = path + os.sep + execName if os.path.exists(guess): success = True result = guess break return { "path" : result, "success" : success } def findGoodTests(): return findExecutable('GoodTests.py') def try_pip_install(): ''' try to pip install GoodTests.py First, try via pip module. If that fails, try to locate pip by dirname(current python executable) + os.sep + pip If that does not exist, scan PATH for pip If found a valid pip executable, invoke it to install GoodTests otherwise, fail. ''' didImport = False try: import pip didImport = True except: pass if didImport is True: print ( "Found pip as module=pip") res = pip.main(['install', 'GoodTests']) if res == 0: return 0 sys.stderr.write('Failed to install GoodTests via pip module. Falling back to pip executable...\n\n') pipPath = os.path.dirname(sys.executable) + os.sep + 'pip' print ( 'Searching for pip at "%s"' %(pipPath, ) ) if not os.path.exists(pipPath): print ( '"%s" does not exist. Scanning PATH to locate a usable pip executable' %(pipPath, )) pipPath = None searchResults = findExecutable('pip') if not searchResults['success']: sys.stderr.write('Failed to find a usable pip executable in PATH.\n') return 1 # Failed to locate a usable pip pipPath = searchResults['path'] print ( 'Found pip executable at "%s"' %(pipPath, ) ) print ( "Executing: %s %s 'install' 'GoodTests'" %(sys.executable, pipPath) ) pipe = subprocess.Popen([sys.executable, pipPath, 'install', 'GoodTests'], shell=False, env=os.environ) res = pipe.wait() return res def download_goodTests(GOODTESTS_URL=None): ''' download_goodTests - Attempts to download GoodTests, using the default global url (or one provided). @return - 0 on success (program should continue), otherwise non-zero (program should abort with this exit status) ''' if GOODTESTS_URL is None: GOODTESTS_URL = globals()['GOODTESTS_URL'] validAnswer = False while validAnswer == False: sys.stdout.write('GoodTests not found. Would you like to install it to local folder? (y/n): ') sys.stdout.flush() answer = sys.stdin.readline().strip().lower() if answer not in ('y', 'n', 'yes', 'no'): continue validAnswer = True answer = answer[0] if answer == 'n': sys.stderr.write('Cannot run tests without installing GoodTests. http://pypi.python.org/pypi/GoodTests or https://github.com/kata198/Goodtests\n') return 1 try: import urllib2 as urllib except ImportError: try: import urllib.request as urllib except: sys.stderr.write('Failed to import urllib. Trying pip.\n') res = try_pip_install() if res != 0: sys.stderr.write('Failed to install GoodTests with pip or direct download. aborting.\n') return 1 try: response = urllib.urlopen(GOODTESTS_URL) contents = response.read() if str != bytes: contents = contents.decode('ascii') except Exception as e: sys.stderr.write('Failed to download GoodTests.py from "%s"\n%s\n' %(GOODTESTS_URL, str(e))) sys.stderr.write('\nTrying pip.\n') res = try_pip_install() if res != 0: sys.stderr.write('Failed to install GoodTests with pip or direct download. aborting.\n') return 1 try: with open('GoodTests.py', 'w') as f: f.write(contents) except Exception as e: sys.stderr.write('Failed to write to GoodTests.py\n%s\n' %(str(e,))) return 1 try: os.chmod('GoodTests.py', 0o775) except: sys.stderr.write('WARNING: Failed to chmod +x GoodTests.py, may not be able to be executed.\n') try: import GoodTests except ImportError: sys.stderr.write('Seemed to download GoodTests okay, but still cannot import. Aborting.\n') return 1 return 0 def main(thisDir=None, additionalArgs=[], MY_PACKAGE_MODULE=None, ALLOW_SITE_INSTALL=None, MY_TEST_DIRECTORY=None, GOODTESTS_URL=None): ''' Do the work - Try to find GoodTests.py, else prompt to download it, then run the tests. @param thisDir - None to use default (directory this test file is in, or if not obtainable, current directory). @param additionalArgs - Any additional args to pass to GoodTests.py Remainder of params take their global (top of file) defaults unless explicitly set here. See top of file for documentation. @return - Exit code of application. 0 on success, non-zero on failure. TODO: Standardize return codes so external applications can derive failure without parsing error strings. ''' if MY_PACKAGE_MODULE is None: MY_PACKAGE_MODULE = globals()['MY_PACKAGE_MODULE'] if ALLOW_SITE_INSTALL is None: ALLOW_SITE_INSTALL = globals()['ALLOW_SITE_INSTALL'] if MY_TEST_DIRECTORY is None: MY_TEST_DIRECTORY = globals()['MY_TEST_DIRECTORY'] if GOODTESTS_URL is None: GOODTESTS_URL = globals()['GOODTESTS_URL'] if not thisDir: thisDir = os.path.dirname(__file__) if not thisDir: thisDir = str(os.getcwd()) elif not thisDir.startswith('/'): thisDir = str(os.getcwd()) + '/' + thisDir # If GoodTests is in current directory, make sure we find it later if os.path.exists('./GoodTests.py'): os.environ['PATH'] = str(os.getcwd()) + ':' + os.environ['PATH'] os.chdir(thisDir) goodTestsInfo = findGoodTests() if goodTestsInfo['success'] is False: downloadRet = download_goodTests(GOODTESTS_URL) if downloadRet != 0: return downloadRet goodTestsInfo = findGoodTests() if goodTestsInfo['success'] is False: sys.stderr.write('Could not download or find GoodTests.py. Try to download it yourself using "pip install GoodTests", or wget %s\n' %( GOODTESTS_URL,)) return 1 baseName = os.path.basename(MY_PACKAGE_MODULE) dirName = os.path.dirname(MY_PACKAGE_MODULE) newPath = None if dirName not in ('.', ''): if dirName.startswith('.'): dirName = os.getcwd() + os.sep + dirName + os.sep newPath = dirName elif dirName == '': inCurrentDir = False try: find_mod(MY_PACKAGE_MODULE) inCurrentDir = True except ImportError: # COMPAT WITH PREVIOUS runTests.py: Try plain module in parent directory foundIt = False oldSysPath = sys.path[:] sys.path = [os.path.realpath(os.getcwd() + os.sep + '..' + os.sep)] try: find_mod(MY_PACKAGE_MODULE) foundIt = True sys.path = oldSysPath except ImportError as e: sys.path = oldSysPath if not ALLOW_SITE_INSTALL: sys.stderr.write('Cannot find "%s" locally.\n' %(MY_PACKAGE_MODULE,)) return 2 else: try: __import__(baseName) except: sys.stderr.write('Cannot find "%s" locally or in global python path.\n' %(MY_PACKAGE_MODULE,)) return 2 if foundIt is True: newPath = os.path.realpath(os.getcwd() + os.sep + '..' + os.sep) if inCurrentDir is True: newPath = os.path.realpath(os.getcwd() + os.sep + '..' + os.sep) if newPath: newPythonPath = [newPath] + [x for x in os.environ.get('PYTHONPATH', '').split(':') if x] os.environ['PYTHONPATH'] = ':'.join(newPythonPath) sys.path = [newPath] + sys.path try: __import__(baseName) except ImportError as e: if baseName.endswith(('.py', '.pyc', '.pyo')): MY_PACKAGE_MODULE = baseName[ : baseName.rindex('.')] try: eName = e.name except AttributeError as noNameE: # Some platforms python2 does not have this attribute # so pull it from the message eName = e.message.split()[-1] if eName != MY_PACKAGE_MODULE: sys.stderr.write('Error while importing %s: %s\n Likely this is another dependency that needs to be installed\nPerhaps run "pip install %s" or install the providing package.\n\n' %(eName, str(e), eName)) return 1 sys.stderr.write('Could not import %s. Either install it or otherwise add to PYTHONPATH\n%s\n' %(MY_PACKAGE_MODULE, str(e))) return 1 if not os.path.isdir(MY_TEST_DIRECTORY): if not os.path.exists(MY_TEST_DIRECTORY): sys.stderr.write('Cannot find test directory: %s\n' %(MY_TEST_DIRECTORY,)) else: sys.stderr.write('Provided test directory, "%s" is not a directory.\n' %(MY_TEST_DIRECTORY,)) return 3 sys.stdout.write('Starting test..\n') sys.stdout.flush() sys.stderr.flush() didTerminate = False pipe = subprocess.Popen([sys.executable, goodTestsInfo['path']] + additionalArgs + [MY_TEST_DIRECTORY], env=os.environ, shell=False) while True: try: pipe.wait() break except KeyboardInterrupt: if not didTerminate: pipe.terminate() didTerminate = True else: pipe.kill() break return 0 if __name__ == '__main__': ret = main(None, sys.argv[1:]) sys.exit(ret)