root/releases/pkgcore-checks/0.3.4/pkgcore_checks/pcheck.py @ ferringb%2540gmail.com-20070130164201-uo3er7u4f4slr1zk

Revision ferringb%2540gmail.com-20070130164201-uo3er7u4f4slr1zk, 21.5 KB (checked in by Brian Harring <ferringb@…>, 2 years ago)

overhaul reporter api a bit, replay now triggers start/end_check properly; simplify streamheader to just dump a ref to the class for checks instead.
Update news

Line 
1# Copyright: 2006 Brian Harring <ferringb@gmail.com>
2# Copyright: 2006 Marien Zwart <marienz@gentoo.org>
3# License: GPL2
4
5
6"""Commandline frontend (for use with L{pkgcore.util.commandline.main}."""
7
8
9from pkgcore.util import commandline, parserestrict, lists, demandload
10from pkgcore.plugin import get_plugins, get_plugin
11from pkgcore_checks import plugins
12from pkgcore.util.formatters import decorate_forced_wrapping
13
14from pkgcore_checks import plugins, base, __version__, feeds
15
16demandload.demandload(globals(), "optparse textwrap os logging "
17    "pkgcore.util:osutils "
18    "pkgcore.restrictions:packages "
19    "pkgcore.restrictions.values:StrExactMatch "
20    "pkgcore.repository:multiplex "
21    "pkgcore.ebuild:repository "
22    "pkgcore_checks:errors "
23    )
24
25
26def repo_callback(option, opt_str, value, parser):
27    try:
28        repo = parser.values.config.repo[value]
29    except KeyError:
30        raise optparse.OptionValueError(
31            'repo %r is not a known repo (known repos: %s)' % (
32                value, ', '.join(repr(n) for n in parser.values.config.repo)))
33    if not isinstance(repo, repository.UnconfiguredTree):
34        raise optparse.OptionValueError(
35            'repo %r is not a pkgcore.ebuild.repository.UnconfiguredTree '
36            'instance; must specify a raw ebuild repo, not type %r: %r' % (
37                value, repo.__class__, repo))
38    setattr(parser.values, option.dest, repo)
39
40
41class OptionParser(commandline.OptionParser):
42
43    """Option parser that is automagically extended by the checks.
44
45    Some comments on the resulting values object:
46
47    - target_repo is passed in as first argument and used as source for
48      packages to check.
49    - src_repo is specified with -r or defaults to target_repo. It is used
50      to get the profiles directory and other non-package repository data.
51    - repo_bases are the path(s) to selected repo(s).
52    - search_repo is a multiplex of target_repo and src_repo if they are
53      different or just target_repo if they are the same. This is used for
54      things like visibility checks (it is passed to the checkers in "start").
55    """
56
57    def __init__(self, **kwargs):
58        commandline.OptionParser.__init__(
59            self, version='pkgcore-checks %s' % (__version__,),
60            description="pkgcore based ebuild QA checks",
61            usage="usage: %prog [options] [atom1...atom2]",
62            **kwargs)
63
64        # These are all set in check_values based on other options, so have
65        # no default set through add_option.
66        self.set_default('repo_bases', [])
67        self.set_default('guessed_target_repo', False)
68        self.set_default('guessed_suite', False)
69        self.set_default('default_suite', False)
70
71        group = self.add_option_group('Check selection')
72        group.add_option(
73            "-c", action="append", type="string", dest="checks_to_run",
74            help="limit checks to those matching this regex, or package/class "
75            "matching; may be specified multiple times")
76        group.set_conflict_handler("resolve")
77        group.add_option("-d",
78            "--disable", action="append", type="string",
79            dest="checks_to_disable", help="specific checks to disable: "
80            "may be specified multiple times")
81        group.set_conflict_handler("error")
82        group.add_option(
83            '--checkset', action='callback', type='string',
84            callback=commandline.config_callback,
85            callback_args=('pcheck_checkset', 'checkset'),
86            help='Pick a preconfigured set of checks to run.')
87
88        self.add_option(
89            '--repo', '-r', action='callback', type='string',
90            callback=repo_callback, dest='target_repo',
91            help='Set the target repo')
92        self.add_option(
93            '--suite', '-s', action='callback', type='string',
94            callback=commandline.config_callback,
95            callback_args=('pcheck_suite', 'suite'),
96            help='Specify the configuration suite to use')
97        self.add_option(
98            "--list-checks", action="store_true", default=False,
99            help="print what checks are available to run and exit")
100        self.add_option(
101            '--reporter', type='string', action='store', default=None,
102            help="Use a non-default reporter (defined in pkgcore's config).")
103        self.add_option(
104            '--list-reporters', action='store_true', default=False,
105            help="print known reporters")
106
107        overlay = self.add_option_group('Overlay')
108        overlay.add_option(
109            '--overlayed-repo', '-o', action='callback', type='string',
110            callback=repo_callback, dest='src_repo',
111            help="if the target repository is an overlay, specify the "
112            "repository name to pull profiles/license from")
113
114        all_addons = set()
115        def add_addon(addon):
116            if addon not in all_addons:
117                all_addons.add(addon)
118                for dep in addon.required_addons:
119                    add_addon(dep)
120        for check in get_plugins('check', plugins):
121            add_addon(check)
122        for addon in all_addons:
123            addon.mangle_option_parser(self)
124
125    def check_values(self, values, args):
126        values, args = commandline.OptionParser.check_values(
127            self, values, args)
128        # XXX hack...
129        values.checks = sorted(get_plugins('check', plugins))
130        if values.list_checks or values.list_reporters:
131            if values.list_reporters == values.list_checks:
132                raise optparse.OptionValueError("--list-checks and "
133                    "--list-reporters are mutually exclusive options- "
134                    "one or the other.")
135            return values, ()
136        cwd = None
137        if values.suite is None:
138            # No suite explicitly specified. Use the repo to guess the suite.
139            if values.target_repo is None:
140                # Not specified either. Try to find a repo our cwd is in.
141                cwd = os.getcwd()
142                # The use of a dict here is a hack to deal with one
143                # repo having multiple names in the configuration.
144                candidates = {}
145                for name, suite in values.config.pcheck_suite.iteritems():
146                    repo = suite.target_repo
147                    if repo is None:
148                        continue
149                    repo_base = getattr(repo, 'base', None)
150                    if repo_base is not None and cwd.startswith(repo_base):
151                        candidates[repo] = name
152                if len(candidates) == 1:
153                    values.guessed_suite = True
154                    values.target_repo = tuple(candidates)[0]
155            if values.target_repo is not None:
156                # We have a repo, now find a suite matching it.
157                candidates = list(
158                    suite for suite in values.config.pcheck_suite.itervalues()
159                    if suite.target_repo is values.target_repo)
160                if len(candidates) == 1:
161                    values.guessed_suite = True
162                    values.suite = candidates[0]
163            if values.suite is None:
164                # If we have multiple candidates or no candidates we
165                # fall back to the default suite.
166                values.suite = values.config.get_default('pcheck_suite')
167                values.default_suite = values.suite is not None
168        if values.suite is not None:
169            # We have a suite. Lift defaults from it for values that
170            # were not set explicitly:
171            if values.checkset is None:
172                values.checkset = values.suite.checkset
173            if values.src_repo is None:
174                values.src_repo = values.suite.src_repo
175            # If we were called with no atoms we want to force
176            # cwd-based detection.
177            if values.target_repo is None:
178                if args:
179                    values.target_repo = values.suite.target_repo
180                elif values.suite.target_repo is not None:
181                    # No atoms were passed in, so we want to guess
182                    # what to scan based on cwd below. That only makes
183                    # sense if we are inside the target repo. We still
184                    # want to pick the suite's target repo if we are
185                    # inside it, in case there is more than one repo
186                    # definition with a base that contains our dir.
187                    if cwd is None:
188                        cwd = os.getcwd()
189                    repo_base = getattr(values.suite.target_repo, 'base', None)
190                    if repo_base is not None and cwd.startswith(repo_base):
191                        values.target_repo = values.suite.target_repo
192        if values.target_repo is None:
193            # We have no target repo (not explicitly passed, not from
194            # a suite, not from an earlier guess at the target_repo).
195            # Try to guess one from cwd:
196            if cwd is None:
197                cwd = os.getcwd()
198            candidates = {}
199            for name, repo in values.config.repo.iteritems():
200                repo_base = getattr(repo, 'base', None)
201                if repo_base is not None and cwd.startswith(repo_base):
202                    candidates[repo] = name
203            if not candidates:
204                self.error(
205                    'No target repo specified on commandline or suite and '
206                    'current directory is not inside a known repo.')
207            elif len(candidates) > 1:
208                self.error(
209                    'Found multiple matches when guessing repo based on '
210                    'current directory (%s). Specify a repo on the '
211                    'commandline or suite or remove some repos from your '
212                    'configuration.' % (
213                        ', '.join(str(repo) for repo in candidates),))
214            values.target_repo = tuple(candidates)[0]
215
216        if values.reporter is None:
217            values.reporter = values.config.get_default(
218                'pcheck_reporter_factory')
219            if values.reporter is None:
220                values.reporter = get_plugin('reporter', plugins)
221            if values.reporter is None:
222                self.error('no config defined reporter found, nor any default '
223                    'plugin based reporters')
224        else:
225            func = values.config.pcheck_reporter_factory.get(values.reporter)
226            if func is None:
227                func = list(base.Whitelist([values.reporter]).filter(
228                    get_plugins('reporter', plugins)))
229                if not func:
230                    self.error("no reporter matches %r\n"
231                        "please see --list-reporter for a list of "
232                        "valid reporters" % values.reporter)
233                elif len(func) > 1:
234                    self.error("--reporter %r matched multiple reporters, "
235                        "must match one. %r" %
236                            (values.reporter,
237                                tuple(sorted("%s.%s" %
238                                    (x.__module__, x.__name__)
239                                    for x in func))
240                            )
241                    )
242                func = func[0]
243            values.reporter = func
244        if values.src_repo is None:
245            values.src_repo = values.target_repo
246            values.search_repo = values.target_repo
247        else:
248            values.search_repo = multiplex.tree(values.target_repo,
249                                                values.src_repo)
250
251        # TODO improve this to deal with a multiplex repo.
252        for repo in set((values.src_repo, values.target_repo)):
253            if isinstance(repo, repository.UnconfiguredTree):
254                values.repo_bases.append(osutils.abspath(repo.base))
255
256        if args:
257            values.limiters = lists.stable_unique(map(
258                    parserestrict.parse_match, args))
259        else:
260            repo_base = getattr(values.target_repo, 'base', None)
261            if not repo_base:
262                self.error(
263                    'Either specify a target repo that is not multi-tree or '
264                    'one or more extended atoms to scan '
265                    '("*" for the entire repo).')
266            cwd = osutils.abspath(os.getcwd())
267            repo_base = osutils.abspath(repo_base)
268            if not cwd.startswith(repo_base):
269                self.error(
270                    'Working dir (%s) is not inside target repo (%s). Fix '
271                    'that or specify one or more extended atoms to scan.' % (
272                        cwd, repo_base))
273            bits = list(p for p in cwd[len(repo_base):].split(os.sep) if p)
274            if not bits:
275                values.limiters = [packages.AlwaysTrue]
276            elif len(bits) == 1:
277                values.limiters = [packages.PackageRestriction(
278                        'category', StrExactMatch(bits[0]))]
279            else:
280                values.limiters = [packages.AndRestriction(
281                        packages.PackageRestriction(
282                            'category', StrExactMatch(bits[0])),
283                        packages.PackageRestriction(
284                            'package', StrExactMatch(bits[1])))]
285
286        if values.checkset is None:
287            values.checkset = values.config.get_default('pcheck_checkset')
288        if values.checkset is not None:
289            values.checks = list(values.checkset.filter(values.checks))
290
291        if values.checks_to_run:
292            whitelist = base.Whitelist(values.checks_to_run)
293            values.checks = list(whitelist.filter(values.checks))
294
295        if values.checks_to_disable:
296            blacklist = base.Blacklist(values.checks_to_disable)
297            values.checks = list(blacklist.filter(values.checks))
298
299        if not values.checks:
300            self.error('No active checks')
301
302        values.addons = set()
303        def add_addon(addon):
304            if addon not in values.addons:
305                values.addons.add(addon)
306                for dep in addon.required_addons:
307                    add_addon(dep)
308        for check in values.checks:
309            add_addon(check)
310        try:
311            for addon in values.addons:
312                addon.check_values(values)
313        except optparse.OptionValueError, e:
314            if values.debug:
315                raise
316            self.error(str(e))
317
318        return values, ()
319
320
321def dump_docstring(out, obj, prefix=None):
322    if prefix is not None:
323        out.first_prefix.append(prefix)
324        out.later_prefix.append(prefix)
325    try:
326        if obj.__doc__ is None:
327            out.write("no documentation")
328            return
329
330        # Docstrings start with an unindented line. Everything
331        # else is consistently indented.
332        lines = obj.__doc__.split('\n')
333        firstline = lines[0].strip()
334        # Some docstrings actually start on the second line.
335        if firstline:
336            out.write(firstline)
337        if len(lines) > 1:
338            for line in textwrap.dedent('\n'.join(lines[1:])).split('\n'):
339                if line:
340                    out.write(line)
341    finally:
342        if prefix is not None:
343            out.first_prefix.pop()
344            out.later_prefix.pop()
345   
346@decorate_forced_wrapping()
347def display_checks(out, checks):
348    d = {}
349    for x in checks:
350        d.setdefault(x.__module__, []).append(x)
351
352    if not d:
353        out.write(out.fg('red'), "No Documentation")
354        out.write()
355        return
356       
357    for module_name in sorted(d):
358        out.write(out.bold, "%s:" % module_name)
359        l = d[module_name]
360        l.sort(key=lambda x:x.__name__)
361
362        try:
363            out.first_prefix.append('  ')
364            out.later_prefix.append('  ')
365            for check in l:
366                out.write("%s:" % check.__name__)
367                dump_docstring(out, check, prefix='  ')
368            out.write()
369        finally:
370            out.first_prefix.pop()
371            out.later_prefix.pop()
372
373
374@decorate_forced_wrapping()
375def display_reporters(out, config, config_reporters, plugin_reporters):
376    out.write("known reporters:")
377    out.write()
378    if config_reporters:
379        out.write("configured reporters:")
380        out.first_prefix.append(' ')
381        out.later_prefix.append(' ')
382        try:
383            # sorting here is random
384            for reporter in sorted(config_reporters, key=lambda x:x.__name__):
385                key = config.get_section_name(reporter)
386                if not key:
387                    continue
388                out.write(out.bold, key)
389                dump_docstring(out, reporter, prefix=' ')
390                out.write()
391        finally:
392            out.first_prefix.pop()
393            out.later_prefix.pop()
394   
395    if plugin_reporters:
396        if config_reporters:
397            out.write()
398        out.write("plugin reporters:")
399        out.first_prefix.append(' ')
400        out.later_prefix.append(' ')
401        try:
402            for reporter in sorted(plugin_reporters, key=lambda x:x.__name__):
403                out.write(out.bold, reporter.__name__)
404                dump_docstring(out, reporter, prefix=' ')
405                out.write()
406        finally:
407            out.first_prefix.pop()
408            out.later_prefix.pop()
409
410    if not plugin_reporters and not config_reporters:
411        out.write(out.fg("red"), "Warning", out.fg(""),
412            " no reporters detected; pcheck won't "
413            "run correctly without a reporter to use!")
414        out.write()
415
416def main(options, out, err):
417    """Do stuff."""
418
419    if options.list_checks:
420        display_checks(out, options.checks)
421        return 0
422
423    if options.list_reporters:
424        display_reporters(out, options.config,
425            options.config.pcheck_reporter_factory.values(),
426            list(get_plugins('reporter', plugins)))
427        return 0   
428
429    if not options.repo_bases:
430        err.write(
431            'Warning: could not determine repository base for profiles. '
432            'Some checks will not work. Either specify a plain target repo '
433            '(not combined trees) or specify a PORTDIR repo '
434            'with --overlayed-repo.', wrap=True)
435        err.write()
436
437    if options.guessed_suite:
438        if options.default_suite:
439            err.write('Tried to guess a suite to use but got multiple matches')
440            err.write('and fell back to the default.')
441        else:
442            err.write('using suite guessed from working directory')
443
444    if options.guessed_target_repo:
445        err.write('using repository guessed from working directory')
446
447    try:
448        reporter = options.reporter(out)
449    except errors.ReporterInitError, e:
450        err.write(err.fg('red'), err.bold, '!!! ', err.reset,
451                  'Error initializing reporter: ', e)
452        return 1
453
454    addons_map = {}
455    def init_addon(klass):
456        res = addons_map.get(klass)
457        if res is not None:
458            return res
459        deps = list(init_addon(dep) for dep in klass.required_addons)
460        try:
461            res = addons_map[klass] = klass(options, *deps)
462        except KeyboardInterrupt:
463            raise
464        except Exception:
465            err.write('instantiating %s' % (klass,))
466            raise
467        return res
468
469    for addon in options.addons:
470        # Ignore the return value, we just need to populate addons_map.
471        init_addon(addon)
472
473    if options.debug:
474        err.write('target repo: ', repr(options.target_repo))
475        err.write('source repo: ', repr(options.src_repo))
476        err.write('base dirs: ', repr(options.repo_bases))
477        for filterer in options.limiters:
478            err.write('limiter: ', repr(filterer))
479        debug = logging.debug
480    else:
481        debug = None
482
483    transforms = list(get_plugins('transform', plugins))
484    # XXX this is pretty horrible.
485    sinks = list(addon for addon in addons_map.itervalues()
486                 if getattr(addon, 'feed_type', False))
487
488    reporter.start()
489
490    for filterer in options.limiters:
491        sources = [feeds.RestrictedRepoSource(options.target_repo, filterer)]
492        bad_sinks, pipes = base.plug(sinks, transforms, sources, debug)
493        if bad_sinks:
494            # We want to report the ones that would work if this was a
495            # full repo scan separately from the ones that are
496            # actually missing transforms.
497            bad_sinks = set(bad_sinks)
498            full_scope = feeds.RestrictedRepoSource(options.target_repo,
499                                                    packages.AlwaysTrue)
500            really_bad, ignored = base.plug(sinks, transforms, [full_scope])
501            really_bad = set(really_bad)
502            assert bad_sinks >= really_bad, \
503                '%r unreachable with no limiters but reachable with?' % (
504                really_bad - bad_sinks,)
505            out_of_scope = bad_sinks - really_bad
506            for sink in really_bad:
507                err.error(
508                    'sink %s could not be connected (missing transforms?)' % (
509                        sink,))
510            for sink in bad_sinks - really_bad:
511                err.warn('not running %s (not a full repo scan)' % (
512                        sink.__class__.__name__,))
513        if not pipes:
514            out.write(out.fg('red'), ' * ', out.reset, 'No checks!')
515        else:
516            err.write('Running %i tests' % (len(sinks) - len(bad_sinks),))
517            for source, pipe in pipes:
518                pipe.start()
519                reporter.start_check(list(base.collect_checks_classes(pipe)),
520                    filterer)
521                for thing in source.feed():
522                    pipe.feed(thing, reporter)
523                pipe.finish(reporter)
524                reporter.end_check()
525
526    reporter.finish()
527
528    # flush stdout first; if they're directing it all to a file, this makes
529    # results not get the final message shoved in midway
530    out.stream.flush()
531    return 0
Note: See TracBrowser for help on using the browser.