1import logging 2import os 3import sys 4import unittest 5 6from nose2 import events, loader, runner, session, util, plugins 7 8 9log = logging.getLogger(__name__) 10__unittest = True 11 12 13class PluggableTestProgram(unittest.TestProgram): 14 15 """TestProgram that enables plugins. 16 17 Accepts the same parameters as :class:`unittest.TestProgram`, 18 but most of them are ignored as their functions are 19 handled by plugins. 20 21 :param module: Module in which to run tests. Default: :func:`__main__` 22 :param defaultTest: Default test name. Default: ``None`` 23 :param argv: Command line args. Default: ``sys.argv`` 24 :param testRunner: *IGNORED* 25 :param testLoader: *IGNORED* 26 :param exit: Exit after running tests? 27 :param verbosity: Base verbosity 28 :param failfast: *IGNORED* 29 :param catchbreak: *IGNORED* 30 :param buffer: *IGNORED* 31 :param plugins: List of additional plugin modules to load 32 :param excludePlugins: List of plugin modules to exclude 33 :param extraHooks: List of hook names and plugin *instances* to 34 register with the session's hooks system. Each 35 item in the list must be a 2-tuple of 36 (hook name, plugin instance) 37 38 .. attribute :: sessionClass 39 40 The class to instantiate to create a test run configuration 41 session. Default: :class:`nose2.session.Session` 42 43 .. attribute :: loaderClass 44 45 The class to instantiate to create a test loader. Default: 46 :class:`nose2.loader.PluggableTestLoader`. 47 48 .. warning :: 49 50 Overriding this attribute is the only way to customize 51 the test loader class. Passing a test loader to 52 :func:`__init__` does not work. 53 54 .. attribute :: runnerClass 55 56 The class to instantiate to create a test runner. Default: 57 :class:`nose2.runner.PluggableTestRunner`. 58 59 .. warning :: 60 61 Overriding this attribute is the only way to customize 62 the test runner class. Passing a test runner to 63 :func:`__init__` does not work. 64 65 .. attribute :: defaultPlugins 66 67 List of default plugin modules to load. 68 69 """ 70 sessionClass = session.Session 71 _currentSession = None 72 loaderClass = loader.PluggableTestLoader 73 runnerClass = runner.PluggableTestRunner 74 defaultPlugins = plugins.DEFAULT_PLUGINS 75 excludePlugins = () 76 77 # XXX override __init__ to warn that testLoader and testRunner are ignored? 78 def __init__(self, **kw): 79 plugins = kw.pop('plugins', []) 80 exclude = kw.pop('excludePlugins', []) 81 hooks = kw.pop('extraHooks', []) 82 self.defaultPlugins = list(self.defaultPlugins) 83 self.excludePlugins = list(self.excludePlugins) 84 self.extraHooks = hooks 85 self.defaultPlugins.extend(plugins) 86 self.excludePlugins.extend(exclude) 87 super(PluggableTestProgram, self).__init__(**kw) 88 89 def parseArgs(self, argv): 90 """Parse command line args 91 92 Parses arguments and creates a configuration session, 93 then calls :func:`createTests`. 94 95 """ 96 self.session = self.sessionClass() 97 self.__class__._currentSession = self.session 98 99 self.argparse = self.session.argparse # for convenience 100 101 # XXX force these? or can it be avoided? 102 self.testLoader = self.loaderClass(self.session) 103 self.session.testLoader = self.testLoader 104 105 # Parse initial arguments like config file paths, verbosity 106 self.setInitialArguments() 107 # FIXME -h here makes processing stop. 108 cfg_args, argv = self.argparse.parse_known_args(argv[1:]) 109 self.handleCfgArgs(cfg_args) 110 111 # Parse arguments for plugins (if any) and test names 112 self.argparse.add_argument('testNames', nargs='*') 113 # add help arg now so -h will also print plugin opts 114 self.argparse.add_argument('-h', '--help', action='help', 115 help=('Show this help message and exit')) 116 args, argv = self.argparse.parse_known_args(argv) 117 if argv: 118 self.argparse.error("Unrecognized arguments: %s" % ' '.join(argv)) 119 self.handleArgs(args) 120 self.createTests() 121 122 def setInitialArguments(self): 123 """Set pre-plugin command-line arguments. 124 125 This set of arguments is parsed out of the command line 126 before plugins are loaded. 127 128 """ 129 self.argparse.add_argument( 130 '-s', '--start-dir', default=None, 131 help="Directory to start discovery ('.' default)") 132 self.argparse.add_argument( 133 '-t', '--top-level-directory', '--project-directory', 134 help='Top level directory of project (defaults to start dir)') 135 self.argparse.add_argument( 136 '--config', '-c', nargs='?', action='append', 137 default=['unittest.cfg', 'nose2.cfg'], 138 help="Config files to load, if they exist. ('unittest.cfg' " 139 "and 'nose2.cfg' in start directory default)") 140 self.argparse.add_argument( 141 '--no-user-config', action='store_const', 142 dest='user_config', const=False, default=True, 143 help="Do not load user config files") 144 self.argparse.add_argument( 145 '--no-plugins', action='store_const', 146 dest='load_plugins', const=False, default=True, 147 help="Do not load any plugins. Warning: nose2 does not " 148 "do anything if no plugins are loaded") 149 self.argparse.add_argument( 150 '--plugin', action='append', 151 dest='plugins', default=[], 152 help="Load this plugin module.") 153 self.argparse.add_argument( 154 '--exclude-plugin', action='append', 155 dest='exclude_plugins', default=[], 156 help="Do not load this plugin module") 157 self.argparse.add_argument( 158 '--verbosity', type=int, 159 help=("Set starting verbosity level (int). " 160 "Applies before -v and -q")) 161 self.argparse.add_argument( 162 '--verbose', '-v', action='count', default=0, 163 help=("Print test case names and statuses. " 164 "Use multiple '-v's for higher verbosity.")) 165 self.argparse.add_argument( 166 '--quiet', '-q', action='count', default=0, dest='quiet', 167 help=("Reduce verbosity. Multiple '-q's result in " 168 "lower verbosity.")) 169 self.argparse.add_argument( 170 '--log-level', default=logging.WARN, 171 help='Set logging level for message logged to console.') 172 173 def handleCfgArgs(self, cfg_args): 174 """Handle initial arguments. 175 176 Handle the initial, pre-plugin arguments parsed out of the 177 command line. 178 179 """ 180 self.session.logLevel = util.parse_log_level(cfg_args.log_level) 181 logging.basicConfig(level=self.session.logLevel) 182 log.debug('logging initialized %s', cfg_args.log_level) 183 if cfg_args.top_level_directory: 184 self.session.topLevelDir = cfg_args.top_level_directory 185 self.session.loadConfigFiles(*self.findConfigFiles(cfg_args)) 186 # set verbosity from config + opts 187 self.session.setVerbosity( 188 cfg_args.verbosity, cfg_args.verbose, cfg_args.quiet) 189 self.session.setStartDir(args_start_dir=cfg_args.start_dir) 190 self.session.prepareSysPath() 191 if cfg_args.load_plugins: 192 self.defaultPlugins.extend(cfg_args.plugins) 193 self.excludePlugins.extend(cfg_args.exclude_plugins) 194 self.loadPlugins() 195 elif cfg_args.plugins or cfg_args.exclude_plugins: 196 log.warn("Both '--no-plugins' and '--plugin' or '--exclude-plugin' " 197 "specified. No plugins were loaded.") 198 199 def findConfigFiles(self, cfg_args): 200 """Find available config files""" 201 filenames = cfg_args.config[:] 202 proj_opts = ('unittest.cfg', 'nose2.cfg') 203 for fn in proj_opts: 204 if cfg_args.top_level_directory: 205 fn = os.path.abspath( 206 os.path.join(cfg_args.top_level_directory, fn)) 207 filenames.append(fn) 208 if cfg_args.user_config: 209 user_opts = ('~/.unittest.cfg', '~/.nose2.cfg') 210 for fn in user_opts: 211 filenames.append(os.path.expanduser(fn)) 212 return filenames 213 214 def handleArgs(self, args): 215 """Handle further arguments. 216 217 Handle arguments parsed out of command line after plugins have 218 been loaded (and injected their argument configuration). 219 220 """ 221 self.testNames = args.testNames 222 self.session.hooks.handleArgs(events.CommandLineArgsEvent(args=args)) 223 224 def loadPlugins(self): 225 """Load available plugins 226 227 228 :func:`self.defaultPlugins`` and :func:`self.excludePlugins` are passed 229 to the session to alter the list of plugins that will be 230 loaded. 231 232 This method also registers any (hook, plugin) pairs set in 233 ``self.hooks``. This is a good way to inject plugins that fall 234 outside of the normal loading procedure, for example, plugins 235 that need some runtime information that can't easily be 236 passed to them through the configuration system. 237 238 """ 239 self.session.loadPlugins(self.defaultPlugins, self.excludePlugins) 240 for method_name, plugin in self.extraHooks: 241 self.session.hooks.register(method_name, plugin) 242 243 def createTests(self): 244 """Create top-level test suite""" 245 event = events.CreateTestsEvent( 246 self.testLoader, self.testNames, self.module) 247 result = self.session.hooks.createTests(event) 248 if event.handled: 249 test = result 250 else: 251 log.debug("Create tests from %s/%s", self.testNames, self.module) 252 test = self.testLoader.loadTestsFromNames( 253 self.testNames, self.module) 254 255 event = events.CreatedTestSuiteEvent(test) 256 result = self.session.hooks.createdTestSuite(event) 257 if event.handled: 258 test = result 259 self.test = test 260 261 def runTests(self): 262 """Run tests""" 263 # fire plugin hook 264 runner = self._makeRunner() 265 try: 266 self.result = runner.run(self.test) 267 except Exception as e: 268 log.exception('Internal Error') 269 sys.stderr.write('Internal Error: runTests aborted: %s\n'%(e)) 270 if self.exit: 271 sys.exit(1) 272 if self.exit: 273 sys.exit(not self.result.wasSuccessful()) 274 275 def _makeRunner(self): 276 runner = self.runnerClass(self.session) 277 event = events.RunnerCreatedEvent(runner) 278 self.session.hooks.runnerCreated(event) 279 self.session.testRunner = event.runner 280 return event.runner 281 282 @classmethod 283 def getCurrentSession(cls): 284 """Returns the current session, or ``None`` if no :class:`nose2.session.Session` is running. 285 286 """ 287 return cls._currentSession 288 289main = PluggableTestProgram 290 291 292def discover(*args, **kwargs): 293 """Main entry point for test discovery. 294 295 Running discover calls :class:`nose2.main.PluggableTestProgram`, 296 passing through all arguments and keyword arguments **except module**: 297 ``module`` is discarded, to force test discovery. 298 299 """ 300 kwargs['module'] = None 301 return main(*args, **kwargs) 302