1import os 2import sys 3import glob 4import optparse 5 6import pyutilib.subprocess 7from pyutilib.th import TestCase 8 9from os.path import dirname 10 11if sys.platform.startswith('win'): 12 platform='win' 13 use_exec = False # Try to use subprocess.run 14else: 15 platform = 'linux' 16 use_exec = True 17 18 19def run(package, basedir, argv, use_exec=use_exec, env=None): 20 if type(package) not in (list, tuple): 21 package = [package] 22 23 parser = optparse.OptionParser(usage='run [OPTIONS] <dirs>') 24 25 parser.add_option( 26 '-v', 27 '--verbose', 28 action='store_true', 29 dest='verbose', 30 default=False, 31 help='Verbose output') 32 parser.add_option( 33 '--cat', 34 '--category', 35 action='append', 36 dest='cat', 37 default=[], 38 help='Specify the test category.') 39 parser.add_option( 40 '--cov', 41 '--coverage', 42 action='store_true', 43 dest='coverage', 44 default=False, 45 help='Enable the computation of coverage information') 46 parser.add_option( 47 '--cover-erase', 48 action='store_true', 49 dest='cover_erase', 50 default=False, 51 help='Erase any previous coverage data files') 52 parser.add_option( 53 '-d', 54 '--dir', 55 action='store', 56 dest='dir', 57 default=None, 58 help='Top-level source directory where the tests are applied.') 59 parser.add_option( 60 '-p', 61 '--package', 62 action='store', 63 dest='pkg', 64 default=package[0], 65 help='Limit the coverage to this package') 66 parser.add_option( 67 '-o', 68 '--output', 69 action='store', 70 dest='output', 71 default=None, 72 help='Redirect output to a file') 73 parser.add_option('--with-doctest', 74 action='store_true', 75 dest='doctests', 76 default=False, 77 help='Run tests included in Sphinx documentation') 78 parser.add_option('--doc-dir', 79 action='store', 80 dest='docdir', 81 default=None, 82 help='Top-level source directory for Sphinx documentation') 83 parser.add_option('--no-xunit', 84 action='store_false', 85 dest='xunit', 86 default=True, 87 help='Disable the nose XUnit plugin') 88 parser.add_option('--dry-run', 89 action='store_true', 90 dest='dryrun', 91 default=False, 92 help='Dry run: collect but do not execute the tests') 93 94 options, args = parser.parse_args(argv) 95 96 if env is None: 97 env = os.environ.copy() 98 99 if options.output: 100 options.output = os.path.abspath(options.output) 101 102 if options.dir is None: 103 os.chdir(basedir) 104 else: 105 os.chdir(options.dir) 106 107 CWD = os.getcwd() 108 print("Running tests in directory %s" % (CWD,)) 109 110 if platform == 'win': 111 binDir = os.path.join(sys.exec_prefix, 'Scripts') 112 nosetests = os.path.join(binDir, 'nosetests.exe') 113 # 114 # JDS [2 Oct 2017]: I am not sure why this was here. If we find 115 # we need it, we can re-add it with an explanation as to why 116 # Windows needs special PYTHONPATH handling. 117 # 118 #srcdirs = [] 119 #for dir in glob.glob('*'): 120 # if os.path.isdir(dir): 121 # srcdirs.append(os.path.abspath(dir)) 122 #if 'PYTHONPATH' in env: 123 # srcdirs.append(env['PYTHONPATH']) 124 #env['PYTHONPATH'] = os.pathsep.join(srcdirs) 125 else: 126 binDir = os.path.join(sys.exec_prefix, 'bin') 127 nosetests = os.path.join(binDir, 'nosetests') 128 129 if os.path.exists(nosetests): 130 cmd = [nosetests] 131 else: 132 cmd = ['nosetests'] 133 134 if (platform == 'win' and sys.version_info[0:2] >= (3, 8)): 135 ####################################################### 136 # This option is required due to a (likely) bug within nosetests. 137 # Nose is no longer maintained, but this workaround is based on a public forum suggestion: 138 # https://stackoverflow.com/questions/58556183/nose-unittest-discovery-broken-on-python-3-8 139 ####################################################### 140 cmd.append('--traverse-namespace') 141 142 if binDir not in env['PATH']: 143 env['PATH'] = os.pathsep.join([binDir, env.get('PATH','')]) 144 145 if options.coverage: 146 cmd.append('--with-coverage') 147 if options.cover_erase: 148 cmd.append('--cover-erase') 149 if options.pkg: 150 cmd.append('--cover-package=%s' % options.pkg) 151 #env['COVERAGE_FILE'] = os.path.join(CWD, '.coverage') 152 153 if options.verbose: 154 cmd.append('-v') 155 if options.dryrun: 156 cmd.append('--collect-only') 157 158 if options.doctests: 159 cmd.extend(['--with-doctest', '--doctest-extension=.rst']) 160 if options.docdir: 161 docdir = os.path.abspath(_options.docdir) 162 if not os.path.exists(docdir): 163 raise ValueError("Invalid documentation directory, " 164 "path does not exist") 165 166 if options.xunit: 167 cmd.append('--with-xunit') 168 cmd.append('--xunit-file=TEST-' + package[0] + '.xml') 169 170 attr = [] 171 _with_performance = False 172 if 'PYUTILIB_UNITTEST_CATEGORY' in env: 173 _categories = TestCase.parse_categories( 174 env.get('PYUTILIB_UNITTEST_CATEGORY', '') ) 175 else: 176 _categories = [] 177 for x in options.cat: 178 _categories.extend( TestCase.parse_categories(x) ) 179 180 # If no one specified a category, default to "smoke" (and anything 181 # not built on pyutilib.th.TestCase) 182 if not _categories: 183 _categories = [ (('smoke',1),), (('pyutilib_th',0),) ] 184 # process each category set (that is, each conjunction of categories) 185 for _category_set in _categories: 186 _attrs = [] 187 # "ALL" deletes the categories, and just runs everything. Note 188 # that "ALL" disables performance testing 189 if ('all', 1) in _category_set: 190 _categories = [] 191 _with_performance = False 192 attr = [] 193 break 194 # For each category set, unless the user explicitly says 195 # something about fragile, assume that fragile should be 196 # EXCLUDED. 197 if ('fragile',1) not in _category_set \ 198 and ('fragile',0) not in _category_set: 199 _category_set = _category_set + (('fragile',0),) 200 # Process each category in the conjection and add to the nose 201 # "attrib" plugin arguments 202 for _category, _value in _category_set: 203 if not _category: 204 continue 205 if _value: 206 _attrs.append(_category) 207 else: 208 _attrs.append("(not %s)" % (_category,)) 209 if _category == 'performance' and _value == 1: 210 _with_performance = True 211 if _attrs: 212 #attr.append('--eval-attr') 213 attr.append("--eval-attr=%s" % (' and '.join(_attrs),)) 214 cmd.extend(attr) 215 if attr: 216 print(" ... for test categor%s: %s" % 217 ('y' if len(attr)<=2 else 'ies', 218 ' '.join(attr[1::2]))) 219 220 if _with_performance: 221 cmd.append('--with-testdata') 222 env['NOSE_WITH_TESTDATA'] = '1' 223 env['NOSE_WITH_FORCED_GC'] = '1' 224 225 targets = set() 226 if len(args) <= 1: 227 targets.update(package) 228 else: 229 for arg in args[1:]: 230 if '*' in arg or '?' in arg: 231 targets.update(glob.glob(arg)) 232 else: 233 targets.add(arg) 234 cmd.extend(list(targets)) 235 236 print("Running...\n %s\n" % ( 237 ' '.join( (x if ' ' not in x else '"'+x+'"') for x in cmd ), )) 238 rc = 0 239 if sys.platform.startswith('java'): 240 import subprocess 241 p = subprocess.Popen(cmd, env=env) 242 p.wait() 243 rc = p.returncode 244 elif options.output: 245 sys.stdout.write("Redirecting output to file '%s' ..." % options.output) 246 rc, _ = pyutilib.subprocess.run(cmd, env=env, outfile=options.output) 247 elif use_exec and not ( 248 sys.platform.startswith('win') 249 and sys.version_info[:2] in ((3,4),(3,5)) ): 250 # NOTE: execvpe seems to generate a fatal error on Windows with 251 # 3.4 and 3.5. 252 # 253 # In other Python versions it fails to return the new process' 254 # return code to the owning shell (so the build harness doesn't 255 # see the command "fail" when there are failing tests. 256 rc = None 257 sys.stderr.flush() 258 sys.stdout.flush() 259 os.execvpe(cmd[0], cmd, env) 260 else: 261 sys.stdout.flush() 262 rc, _ = pyutilib.subprocess.run(cmd, env=env, ostream=sys.stdout) 263 return rc 264 265 266def runPyUtilibTests(argv=None, use_exec=use_exec): 267 if argv is None: 268 argv = sys.argv 269 270 return pyutilib.dev.runtests.run( 271 'pyutilib', 272 dirname(dirname(dirname(os.path.abspath(__file__)))), 273 argv, 274 use_exec=use_exec ) 275