1 2# -*- coding: utf-8 -*- 3 4# Script to run some or all PyGeodesy tests with Python 2 or 3. 5 6# Tested with 64-bit Python 2.6.9, 2.7,13, 3.5.3, 3.6.4, 3.6.5 and 7# 3.7.0 on macOS 10.12 Sierra, 10.13 High Sierra, 10.15.5-7 Catalina 8# and 11.0, 11.1 and 11.2 (10.16) Big Sur and with Pythonista 3.1 9# and 3.2 on iOS 10.3, 11.0, 11.1, 11.3 and 11.4. 10 11from base import clips, coverage, isiOS, isPython3, PyGeodesy_dir, PythonX, \ 12 secs2str, test_dir, tilde, versions, _W_opts # PYCHOK expected 13 14from os import access, environ, F_OK, linesep as NL 15import sys 16 17__all__ = ('run2',) 18__version__ = '21.08.14' 19 20if isiOS: # MCCABE 14 21 22 try: # prefer StringIO over io 23 from StringIO import StringIO 24 except ImportError: # Python 3+ 25 from io import StringIO 26 from os.path import basename 27 from runpy import run_path 28 from traceback import format_exception 29 30 def run2(test, *unused): 31 '''Invoke one test module and return 32 the exit status and console output. 33 ''' 34 # Mimick partial behavior of function run2 35 # further below because subprocess.Popen is 36 # not available on iOS/Pythonista/Python. 37 # One issue however, test scripts are all 38 # imported and run in this same process. 39 40 x = None # no exit, no exception 41 42 sys3 = sys.argv, sys.stdout, sys.stderr 43 sys.stdout = sys.stderr = std = StringIO() 44 try: 45 sys.argv = [test] 46 run_path(test, run_name='__main__') 47 except: # PYCHOK have to on Pythonista 48 x = sys.exc_info() 49 if x[0] is SystemExit: 50 x = x[1].code # exit status 51 else: # append traceback 52 x = [t for t in format_exception(*x) 53 if 'runpy.py", line' not in t] 54 print(''.join(map(tilde, x)).rstrip()) 55 x = 1 # count as a failure 56 sys.argv, sys.stdout, sys.stderr = sys3 57 58 r = _decoded(std.getvalue()) 59 60 std.close() 61 std = None # del std 62 63 if x is None: # no exit status or exception: 64 # count failed tests excluding KNOWN ones 65 x = r.count('FAILED, expected') 66 return x, r 67 68 PythonX_O = basename(PythonX) 69 70else: # non-iOS 71 72 from subprocess import PIPE, STDOUT, Popen 73 74 # replace home dir with ~ 75 PythonX_O = PythonX.replace(environ.get('HOME', '~'), '~') 76 pythonC_ = (PythonX,) # python cmd tuple 77 if not __debug__: 78 PythonX_O += ' -O' 79 pythonC_ += ('-O',) 80 if _W_opts: # include -W options 81 PythonX_O += ' ' + _W_opts 82 pythonC_ += (_W_opts,) 83 if coverage: 84 pythonC_ += tuple('-m coverage run -a'.split()) 85 86 def run2(test, *opts): # PYCHOK expected 87 '''Invoke one test module and return 88 the exit status and console output. 89 ''' 90 c = pythonC_ + (test,) + opts 91 p = Popen(c, creationflags=0, 92 executable =sys.executable, 93 # shell =True, 94 stdin =None, 95 stdout =PIPE, # XXX 96 stderr =STDOUT) # XXX 97 98 r = _decoded(p.communicate()[0]) 99 100 # the exit status reflects the number of 101 # test failures in the tested module 102 return p.returncode, r 103 104# shorten Python path [-O] 105PythonX_O = clips(PythonX_O, 32) 106 107# command line options 108_failedonly = False 109_raiser = False 110_results = False # or file 111_verbose = False 112_Total = 0 # total tests 113_FailX = 0 # failed tests 114 115 116if isPython3: 117 def _decoded(r): 118 return r.decode('utf-8') if isinstance(r, bytes) else r 119 120 def _encoded(r): 121 return r.encode('utf-8') if isinstance(r, str) else r 122 123else: # avoids UnicodeDecodeError 124 def _decoded(r): # PYCHOK redefined 125 return r 126 127 def _encoded(r): # PYCHOK redefined 128 return r 129 130 131def _exit(last, text, exit): 132 '''(INTERNAL) Close and exit. 133 ''' 134 print(last) 135 if _results: 136 _write(NL + text + NL) 137 _results.close() 138 sys.exit(exit) 139 140 141def _run(test, *opts): # MCCABE 13 142 '''(INTERNAL) Run a test script and parse the result. 143 ''' 144 global _Total, _FailX 145 146 t = 'running %s %s' % (PythonX_O, tilde(test)) 147 if access(test, F_OK): 148 149 print(t) 150 x, r = run2(test, *opts) 151 r = r.replace(PyGeodesy_dir, '.') 152 if _results: 153 _write(NL + t + NL) 154 _write(r) 155 156 if 'Traceback' in r: 157 print(r + NL) 158 if not x: # count as failure 159 _FailX += 1 160 if _raiser: 161 raise SystemExit 162 163 elif _failedonly: 164 for t in _testlines(r): 165 if ', KNOWN' not in t: 166 print(t) 167 168 elif _verbose: 169 print(r + NL) 170 171 elif x: 172 for t in _testlines(r): 173 print(t) 174 175 else: 176 r = t + ' FAILED: no such file' + NL 177 x = 1 178 if _results: 179 _write(NL + r) 180 print(r) 181 182 _Total += r.count(NL + ' test ') # number of tests 183 _FailX += x # failures, excluding KNOWN ones 184 185 186def _testlines(r): 187 '''(INTERNAL) Yield test lines. 188 ''' 189 for t in r.split(NL): 190 if 'FAILED,' in t or 'passed' in t or 'SKIPPED' in t: 191 yield t.rstrip() 192 yield '' 193 194 195def _write(text): 196 '''(INTERNAL) Write text to results. 197 ''' 198 _results.write(_encoded(text)) 199 200 201if __name__ == '__main__': # MCCABE 19 202 203 from glob import glob 204 from os.path import join 205 from time import time 206 207 argv0, args = tilde(sys.argv[0]), sys.argv[1:] 208 209 while args and args[0].startswith('-'): 210 arg = args.pop(0) 211 if '-help'.startswith(arg): 212 print('usage: %s [-B] [-failedonly] [-raiser] [-results] [-verbose] [-Z[0-9]] [test/test...py ...]' % (argv0,)) 213 sys.exit(0) 214 elif arg.startswith('-B'): 215 environ['PYTHONDONTWRITEBYTECODE'] = arg[2:] 216 elif '-failedonly'.startswith(arg): 217 _failedonly = True 218 elif '-raiser'.startswith(arg): 219 _raiser = True # break on error 220 elif '-results'.startswith(arg): 221 _results = True 222 elif '-verbose'.startswith(arg): 223 _verbose = True 224 elif arg.startswith('-Z'): 225 environ['PYGEODESY_LAZY_IMPORT'] = arg[2:] 226 else: 227 print('%s invalid option: %s' % (argv0, arg)) 228 sys.exit(1) 229 230 if not args: # no tests specified, get all test*.py 231 # scripts in the same directory as this one 232 args = sorted(glob(join(test_dir, 'test[A-Z]*.py'))) 233 234 # PyGeodesy and Python versions, size, OS name and release 235 v = versions() 236 237 if _results: # save all test results 238 t = '-'.join(['testresults'] + v.split()) + '.txt' 239 t = join(PyGeodesy_dir, 'testresults', t) 240 _results = open(t, 'wb') # note, 'b' not 't'! 241 _write('%s typical test results (%s)%s' % (argv0, v, NL)) 242 243 s = time() 244 try: 245 for arg in args: 246 _run(*arg.split()) 247 except KeyboardInterrupt: 248 _exit('', '^C', 9) 249 except SystemExit: 250 pass 251 s = time() - s 252 t = secs2str(s) 253 if _Total > s > 1: 254 t = '%s (%.3f tps)' % (t, _Total / s) 255 256 if _FailX: 257 s = '' if _FailX == 1 else 's' 258 x = '%d (of %d) test%s FAILED' % (_FailX, _Total, s) 259 elif _Total > 0: 260 x = 'all %d tests OK' % (_Total,) 261 else: 262 x = 'all OK' 263 264 t = '%s %s %s (%s) %s' % (argv0, PythonX_O, x, v, t) 265 _exit(t, t, 2 if _FailX else 0) 266 267# **) MIT License 268# 269# Copyright (C) 2016-2021 -- mrJean1 at Gmail -- All Rights Reserved. 270# 271# Permission is hereby granted, free of charge, to any person obtaining a 272# copy of this software and associated documentation files (the "Software"), 273# to deal in the Software without restriction, including without limitation 274# the rights to use, copy, modify, merge, publish, distribute, sublicense, 275# and/or sell copies of the Software, and to permit persons to whom the 276# Software is furnished to do so, subject to the following conditions: 277# 278# The above copyright notice and this permission notice shall be included 279# in all copies or substantial portions of the Software. 280# 281# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 282# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 283# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 284# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 285# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 286# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 287# OTHER DEALINGS IN THE SOFTWARE. 288