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