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