1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Name:         testRunner.py
4# Purpose:      Music21 testing suite
5#
6# Authors:      Michael Scott Cuthbert
7#               Christopher Ariza
8#
9# Copyright:    Copyright © 2006-2016 Michael Scott Cuthbert and the music21
10#               Project
11# License:      BSD, see license.txt
12# -----------------------------------------------------------------------------
13'''
14The testRunner module contains the all important "mainTest" function that runs tests
15in a given module.  Except for the one instance of "defaultImports", everything here
16can run on any system, not just music21.
17'''
18import doctest
19import inspect
20import platform
21import re
22import sys
23import unittest
24
25defaultImports = ['music21']
26
27
28# ALL_OUTPUT = []
29
30# test related functions
31
32def addDocAttrTestsToSuite(suite,
33                           moduleVariableLists,
34                           outerFilename=None,
35                           globs=False,
36                           optionflags=(
37                               doctest.ELLIPSIS
38                               | doctest.NORMALIZE_WHITESPACE
39                           )):
40    '''
41    takes a suite, such as a doctest.DocTestSuite and the list of variables
42    in a module and adds from those classes that have a _DOC_ATTR dictionary
43    (which documents the properties in the class) any doctests to the suite.
44
45    >>> import doctest
46    >>> s1 = doctest.DocTestSuite(chord)
47    >>> s1TestsBefore = len(s1._tests)
48    >>> allLocals = [getattr(chord, x) for x in dir(chord)]
49    >>> test.testRunner.addDocAttrTestsToSuite(s1, allLocals)
50    >>> s1TestsAfter = len(s1._tests)
51    >>> s1TestsAfter - s1TestsBefore
52    2
53    >>> t = s1._tests[-1]
54    >>> t
55    isRest ()
56    '''
57    dtp = doctest.DocTestParser()
58    if globs is False:
59        globs = __import__(defaultImports[0]).__dict__.copy()
60
61    elif globs is None:
62        globs = {}
63
64    for lvk in moduleVariableLists:
65        if not (inspect.isclass(lvk)):
66            continue
67        docattr = getattr(lvk, '_DOC_ATTR', None)
68        if docattr is None:
69            continue
70        for dockey in docattr:
71            documentation = docattr[dockey]
72            # print(documentation)
73            dt = dtp.get_doctest(documentation, globs, dockey, outerFilename, 0)
74            if not dt.examples:
75                continue
76            dtc = doctest.DocTestCase(dt,
77                                      optionflags=optionflags,
78                                      )
79            # print(dtc)
80            suite.addTest(dtc)
81
82
83def fixDoctests(doctestSuite):
84    r'''
85    Fix doctests so that addresses are sanitized.
86
87    In the past this fixed other differences among Python versions.
88    In the future, it might again!
89    '''
90    windows: bool = platform.system() == 'Windows'
91    for dtc in doctestSuite:  # Suite to DocTestCase -- undocumented.
92        if not hasattr(dtc, '_dt_test'):
93            continue
94
95        dt = dtc._dt_test  # DocTest
96        for example in dt.examples:
97            example.want = stripAddresses(example.want, '0x...')
98            if windows:
99                example.want = example.want.replace('PosixPath', 'WindowsPath')
100
101
102ADDRESS = re.compile('0x[0-9A-Fa-f]+')
103
104
105def stripAddresses(textString, replacement='ADDRESS') -> str:
106    '''
107    Function that changes all memory addresses (pointers) in the given
108    textString with (replacement).  This is useful for testing
109    that a function gives an expected result even if the result
110    contains references to memory locations.  So for instance:
111
112    >>> stripA = test.testRunner.stripAddresses
113    >>> stripA('{0.0} <music21.clef.TrebleClef object at 0x02A87AD0>')
114    '{0.0} <music21.clef.TrebleClef object at ADDRESS>'
115
116    while this is left alone:
117
118    >>> stripA('{0.0} <music21.humdrum.spineParser.MiscTandem *>I>')
119    '{0.0} <music21.humdrum.spineParser.MiscTandem *>I>'
120
121
122    For doctests, can strip to '...' to make it work fine with doctest.ELLIPSIS
123
124    >>> stripA('{0.0} <music21.base.Music21Object object at 0x102a0ff10>', '0x...')
125    '{0.0} <music21.base.Music21Object object at 0x...>'
126    '''
127    return ADDRESS.sub(replacement, textString)
128
129
130# ------------------------------------------------------------------------------
131
132def mainTest(*testClasses, **kwargs):
133    '''
134    Takes as its arguments modules (or a string 'noDocTest' or 'verbose')
135    and runs all of these modules through a unittest suite
136
137    Unless 'noDocTest' is passed as a module, a docTest
138    is also performed on `__main__`, hence the name "mainTest".
139
140    If 'moduleRelative' (a string) is passed as a module, then
141    global variables are preserved.
142
143    Run example (put at end of your modules):
144
145    ::
146
147        import unittest
148        class Test(unittest.TestCase):
149            def testHello(self):
150                hello = 'Hello'
151                self.assertEqual('Hello', hello)
152
153        import music21
154        if __name__ == '__main__':
155            music21.mainTest(Test)
156
157
158    This module tries to fix up some differences between python2 and python3 so
159    that the same doctests can work.  These differences can now be removed, but
160    I cannot remember what they are!
161    '''
162
163    runAllTests = True
164
165    # default -- is fail fast.
166    failFast = bool(kwargs.get('failFast', True))
167    if failFast:
168        optionflags = (
169            doctest.ELLIPSIS
170            | doctest.NORMALIZE_WHITESPACE
171            | doctest.REPORT_ONLY_FIRST_FAILURE
172        )
173    else:
174        optionflags = (
175            doctest.ELLIPSIS
176            | doctest.NORMALIZE_WHITESPACE
177        )
178
179    globs = None
180    if ('noDocTest' in testClasses
181            or 'noDocTest' in sys.argv
182            or 'nodoctest' in sys.argv
183            or bool(kwargs.get('noDocTest', False))):
184        skipDoctest = True
185    else:
186        skipDoctest = False
187
188    # start with doc tests, then add unit tests
189    if skipDoctest:
190        # create a test suite for storage
191        s1 = unittest.TestSuite()
192    else:
193        # create test suite derived from doc tests
194        # here we use '__main__' instead of a module
195        if ('moduleRelative' in testClasses
196                or 'moduleRelative' in sys.argv
197                or bool(kwargs.get('moduleRelative', False))):
198            pass
199        else:
200            for di in defaultImports:
201                globs = __import__(di).__dict__.copy()
202            if ('importPlusRelative' in testClasses
203                    or 'importPlusRelative' in sys.argv
204                    or bool(kwargs.get('importPlusRelative', False))):
205                globs.update(inspect.stack()[1][0].f_globals)
206
207        try:
208            s1 = doctest.DocTestSuite(
209                '__main__',
210                globs=globs,
211                optionflags=optionflags,
212            )
213        except ValueError as ve:  # no docstrings
214            print('Problem in docstrings [usually a missing r value before '
215                  + f'the quotes:] {ve}')
216            s1 = unittest.TestSuite()
217
218    verbosity = 1
219    if ('verbose' in testClasses
220            or 'verbose' in sys.argv
221            or bool(kwargs.get('verbose', False))):
222        verbosity = 2  # this seems to hide most display
223
224    displayNames = False
225    if ('list' in sys.argv
226            or 'display' in sys.argv
227            or bool(kwargs.get('display', False))
228            or bool(kwargs.get('list', False))):
229        displayNames = True
230        runAllTests = False
231
232    runThisTest = None
233    if len(sys.argv) == 2:
234        arg = sys.argv[1].lower()
235        if arg not in ('list', 'display', 'verbose', 'nodoctest'):
236            # run a test directly named in this module
237            runThisTest = sys.argv[1]
238    if bool(kwargs.get('runTest', False)):
239        runThisTest = kwargs.get('runTest', False)
240
241    # -f, --failfast
242    if ('onlyDocTest' in sys.argv
243            or 'onlyDocTest' in testClasses
244            or bool(kwargs.get('onlyDocTest', False))):
245        testClasses = []  # remove cases
246    for t in testClasses:
247        if not isinstance(t, str):
248            if displayNames is True:
249                for tName in unittest.defaultTestLoader.getTestCaseNames(t):
250                    print(f'Unit Test Method: {tName}')
251            if runThisTest is not None:
252                tObj = t()  # call class
253                # search all names for case-insensitive match
254                for name in dir(tObj):
255                    if (name.lower() == runThisTest.lower()
256                           or name.lower() == ('test' + runThisTest.lower())
257                           or name.lower() == ('xtest' + runThisTest.lower())):
258                        runThisTest = name
259                        break
260                if hasattr(tObj, runThisTest):
261                    print(f'Running Named Test Method: {runThisTest}')
262                    tObj.setUp()
263                    getattr(tObj, runThisTest)()
264                    runAllTests = False
265                    break
266                else:
267                    print(f'Could not find named test method: {runThisTest}, running all tests')
268
269            # normally operation collects all tests
270            s2 = unittest.defaultTestLoader.loadTestsFromTestCase(t)
271            s1.addTests(s2)
272
273    # Add _DOC_ATTR tests...
274    if not skipDoctest:
275        stacks = inspect.stack()
276        if len(stacks) > 1:
277            outerFrameTuple = stacks[1]
278        else:
279            outerFrameTuple = stacks[0]
280        outerFrame = outerFrameTuple[0]
281        outerFilename = outerFrameTuple[1]
282        localVariables = list(outerFrame.f_locals.values())
283        addDocAttrTestsToSuite(s1, localVariables, outerFilename, globs, optionflags)
284
285    if runAllTests is True:
286        fixDoctests(s1)
287
288        runner = unittest.TextTestRunner()
289        runner.verbosity = verbosity
290        unused_testResult = runner.run(s1)
291
292
293if __name__ == '__main__':
294    mainTest()
295    # from pprint import pprint
296    # pprint(ALL_OUTPUT)
297