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