aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.rst4
-rw-r--r--pkgcheck/base.py13
-rw-r--r--pkgcheck/reporters.py15
-rw-r--r--pkgcheck/scripts/pkgcheck.py414
-rw-r--r--pkgcheck/test/test_pkgcheck.py2
5 files changed, 267 insertions, 181 deletions
diff --git a/README.rst b/README.rst
index d1ace520..e31d639a 100644
--- a/README.rst
+++ b/README.rst
@@ -123,10 +123,10 @@ ones. Examples::
patterns=unstable_only stale_unstable imlate
The first disables the three specified checks, the second enables only
-those three. For available names see ``pkgcheck --list-checks``.
+those three. For available names see ``pkgcheck show --checks``.
``patterns`` is a whitespace-separated list. If the values are strings
-they need to match a component of the name in ``--list-checks``
+they need to match a component of the name in ``pkgcheck show --checks``
exactly. If it looks like a regexp (currently defined as "contains a +
or \*") this needs to match the entire name.
diff --git a/pkgcheck/base.py b/pkgcheck/base.py
index 3b87385a..6e770931 100644
--- a/pkgcheck/base.py
+++ b/pkgcheck/base.py
@@ -377,6 +377,19 @@ class Suite(object):
self.checkset = checkset
+class StreamHeader(object):
+
+ def __init__(self, checks, criteria):
+ self.checks = sorted((x for x in checks if x.known_results),
+ key=lambda x: x.__name__)
+ self.known_results = set()
+ for x in checks:
+ self.known_results.update(x.known_results)
+
+ self.known_results = tuple(sorted(self.known_results))
+ self.criteria = str(criteria)
+
+
class CheckRunner(object):
def __init__(self, checks):
diff --git a/pkgcheck/reporters.py b/pkgcheck/reporters.py
index 95d9e939..e9fec9a2 100644
--- a/pkgcheck/reporters.py
+++ b/pkgcheck/reporters.py
@@ -191,19 +191,6 @@ class XmlReporter(base.Reporter):
self.out.write('</checks>')
-class StreamHeader(object):
-
- def __init__(self, checks, criteria):
- self.checks = sorted((x for x in checks if x.known_results),
- key=lambda x: x.__name__)
- self.known_results = set()
- for x in checks:
- self.known_results.update(x.known_results)
-
- self.known_results = tuple(sorted(self.known_results))
- self.criteria = str(criteria)
-
-
class PickleStream(base.Reporter):
"""Generate a stream of pickled objects.
@@ -227,7 +214,7 @@ class PickleStream(base.Reporter):
self.out.autoline = False
def start_check(self, checks, target):
- self.dump(StreamHeader(checks, target), self.out)
+ self.dump(base.StreamHeader(checks, target), self.out)
def process_report(self, result):
try:
diff --git a/pkgcheck/scripts/pkgcheck.py b/pkgcheck/scripts/pkgcheck.py
index bbb4274d..b411a0d1 100644
--- a/pkgcheck/scripts/pkgcheck.py
+++ b/pkgcheck/scripts/pkgcheck.py
@@ -8,6 +8,7 @@ from itertools import chain
from pkgcore.plugin import get_plugins, get_plugin
from pkgcore.util import commandline, parserestrict
+from snakeoil.cli import arghparse
from snakeoil.demandload import demandload
from snakeoil.formatters import decorate_forced_wrapping
from snakeoil.sequences import unstable_unique
@@ -23,23 +24,28 @@ demandload(
'pkgcore.restrictions:packages',
'pkgcore.restrictions.values:StrExactMatch',
'pkgcore.repository:multiplex',
+ 'snakeoil:pickling,formatters',
'snakeoil.osutils:abspath',
'snakeoil.sequences:iflatten_instance',
'snakeoil.strings:pluralism',
'pkgcheck:errors',
)
-argparser = commandline.ArgumentParser(
- domain=False, color=False, description=__doc__, script=(__file__, __name__))
+pkgcore_opts = commandline.ArgumentParser(domain=False, script=(__file__, __name__))
+argparser = arghparse.ArgumentParser(
+ suppress=True, color=False, description=__doc__, parents=(pkgcore_opts,))
+subparsers = argparser.add_subparsers(description="check applets")
+
# These are all set based on other options, so have no default setting.
-argparser.set_defaults(repo_bases=[])
-argparser.set_defaults(guessed_target_repo=False)
-argparser.set_defaults(guessed_suite=False)
-argparser.set_defaults(default_suite=False)
-argparser.add_argument(
+scan = subparsers.add_parser('scan', description='scan targets for QA issues')
+scan.set_defaults(repo_bases=[])
+scan.set_defaults(guessed_target_repo=False)
+scan.set_defaults(guessed_suite=False)
+scan.set_defaults(default_suite=False)
+scan.add_argument(
'targets', metavar='TARGET', nargs='*', help='optional target atom(s)')
-main_options = argparser.add_argument_group('main options')
+main_options = scan.add_argument_group('main options')
main_options.add_argument(
'-r', '--repo', metavar='REPO', dest='target_repo',
action=commandline.StoreRepoObject,
@@ -54,38 +60,10 @@ main_options.add_argument(
docs="""
Select a reporter to use for scan output.
- Use --list-reporters to see available options.
- """)
-list_options = main_options.add_mutually_exclusive_group()
-list_options.add_argument(
- '--list-keywords', action='store_true', default=False,
- help='show available warning/error keywords and exit',
- docs="""
- List all available keywords and exit.
-
- Use -v/--verbose to show keywords sorted into the scope they run at
- (repository, category, package, or version) along with their
- descriptions.
- """)
-list_options.add_argument(
- '--list-checks', action='store_true', default=False,
- help='show available checks and exit',
- docs="""
- List all available checks and exit.
-
- Use -v/--verbose to show descriptions and possible keyword results for
- each check.
- """)
-list_options.add_argument(
- '--list-reporters', action='store_true', default=False,
- help='show available reporters and exit',
- docs="""
- List all available reporters and exit.
-
- Use -v/--verbose to show reporter descriptions.
+ Use 'pkgcheck show --reporters' to see available options.
""")
-check_options = argparser.add_argument_group('check selection')
+check_options = scan.add_argument_group('check selection')
check_options.add_argument(
'-c', '--checks', metavar='CHECK', action='extend_comma_toggle', dest='selected_checks',
help='limit checks to regex or package/class matching (comma-separated list)')
@@ -111,7 +89,7 @@ check_options.add_argument(
scan for errors and ignore all QA warnings use the 'errors' argument to
-k/--keywords.
- Use --list-keywords or the list below to see available options.
+ Use 'pkgcheck show --keywords' or the list below to see available options.
""")
check_options.add_argument(
'-S', '--scopes', metavar='SCOPE', action='extend_comma_toggle', dest='selected_scopes',
@@ -136,11 +114,11 @@ def add_addon(addon, addon_set):
all_addons = set()
-argparser.plugin = argparser.add_argument_group('plugin options')
+scan.plugin = scan.add_argument_group('plugin options')
for check in get_plugins('check', plugins):
add_addon(check, all_addons)
for addon in all_addons:
- addon.mangle_argparser(argparser)
+ addon.mangle_argparser(scan)
# XXX hack...
_known_checks = tuple(sorted(
@@ -152,12 +130,9 @@ _known_keywords = tuple(sorted(
key=lambda x: x.__name__))
-@argparser.bind_final_check
+@scan.bind_final_check
def _validate_args(parser, namespace):
- if any((namespace.list_keywords, namespace.list_checks, namespace.list_reporters)):
- # no need to check any other args
- return
-
+ namespace.targets = []
namespace.enabled_checks = list(_known_checks)
namespace.enabled_keywords = list(_known_keywords)
@@ -359,25 +334,25 @@ def _validate_args(parser, namespace):
available_keywords = set(x.__name__ for x in _known_keywords)
unknown_keywords = selected_keywords - available_keywords
if unknown_keywords:
- parser.error('unknown keyword%s: %s (use --list-keywords to show valid keywords)' % (
+ parser.error("unknown keyword%s: %s (use 'pkgcheck show --keywords' to show valid keywords)" % (
pluralism(unknown_keywords), ', '.join(unknown_keywords)))
# filter outputted keywords
namespace.enabled_keywords = base.filter_update(
namespace.enabled_keywords, enabled_keywords, disabled_keywords)
- namespace.enabled_keywords = set(namespace.enabled_keywords)
- if namespace.enabled_keywords == set(_known_keywords):
- namespace.enabled_keywords = None
+ namespace.filtered_keywords = set(namespace.enabled_keywords)
+ if namespace.filtered_keywords == set(_known_keywords):
+ namespace.filtered_keywords = None
disabled_checks, enabled_checks = ((), ())
if namespace.selected_checks is not None:
disabled_checks, enabled_checks = namespace.selected_checks
- elif namespace.enabled_keywords is not None:
+ elif namespace.filtered_keywords is not None:
# enable checks based on enabled keyword -> check mapping
enabled_checks = []
for check in _known_checks:
- if namespace.enabled_keywords.intersection(check.known_results):
+ if namespace.filtered_keywords.intersection(check.known_results):
enabled_checks.append(check.__name__)
# filter checks to run
@@ -404,6 +379,196 @@ def _validate_args(parser, namespace):
parser.error(str(e))
+@scan.bind_main_func
+def _scan(options, out, err):
+ """Do stuff."""
+ if not options.repo_bases:
+ err.write(
+ 'Warning: could not determine repo base for profiles, some checks will not work.')
+ err.write()
+
+ if options.guessed_suite:
+ if options.default_suite:
+ err.write('Tried to guess a suite to use but got multiple matches')
+ err.write('and fell back to the default.')
+ else:
+ err.write('using suite guessed from working directory')
+
+ if options.guessed_target_repo:
+ err.write('using repository guessed from working directory')
+
+ try:
+ reporter = options.reporter(
+ out, keywords=options.filtered_keywords, verbose=options.verbose)
+ except errors.ReporterInitError as e:
+ err.write(
+ err.fg('red'), err.bold, '!!! ', err.reset,
+ 'Error initializing reporter: ', e)
+ return 1
+
+ addons_map = {}
+
+ def init_addon(klass):
+ res = addons_map.get(klass)
+ if res is not None:
+ return res
+ deps = list(init_addon(dep) for dep in klass.required_addons)
+ try:
+ res = addons_map[klass] = klass(options, *deps)
+ except KeyboardInterrupt:
+ raise
+ except Exception:
+ if options.debug:
+ err.write('instantiating %s' % (klass,))
+ raise
+ return res
+
+ for addon in options.addons:
+ # Ignore the return value, we just need to populate addons_map.
+ init_addon(addon)
+
+ if options.verbose:
+ err.write("target repo: '%s' at '%s'" % (
+ options.target_repo.repo_id, options.target_repo.location))
+ err.write('base dirs: ', ', '.join(options.repo_bases))
+ for filterer in options.limiters:
+ err.write('limiter: ', filterer)
+ debug = logging.debug
+ else:
+ debug = None
+
+ transforms = list(get_plugins('transform', plugins))
+ # XXX this is pretty horrible.
+ sinks = list(addon for addon in addons_map.itervalues()
+ if getattr(addon, 'feed_type', False))
+
+ reporter.start()
+
+ for filterer in options.limiters:
+ sources = [feeds.RestrictedRepoSource(options.target_repo, filterer)]
+ bad_sinks, pipes = base.plug(sinks, transforms, sources, debug)
+ if bad_sinks:
+ # We want to report the ones that would work if this was a
+ # full repo scan separately from the ones that are
+ # actually missing transforms.
+ bad_sinks = set(bad_sinks)
+ full_scope = feeds.RestrictedRepoSource(
+ options.target_repo, packages.AlwaysTrue)
+ really_bad, ignored = base.plug(sinks, transforms, [full_scope])
+ really_bad = set(really_bad)
+ assert bad_sinks >= really_bad, \
+ '%r unreachable with no limiters but reachable with?' % (
+ really_bad - bad_sinks,)
+ for sink in really_bad:
+ err.error(
+ 'sink %s could not be connected (missing transforms?)' % (
+ sink,))
+ out_of_scope = bad_sinks - really_bad
+ if options.verbose and out_of_scope:
+ err.warn('skipping repo checks (not a full repo scan)')
+ if not pipes:
+ out.write(out.fg('red'), ' * ', out.reset, 'No checks!')
+ else:
+ if options.debug:
+ err.write('Running %i tests' % (len(sinks) - len(bad_sinks),))
+ for source, pipe in pipes:
+ pipe.start()
+ reporter.start_check(
+ list(base.collect_checks_classes(pipe)), filterer)
+ for thing in source.feed():
+ pipe.feed(thing, reporter)
+ pipe.finish(reporter)
+ reporter.end_check()
+
+ reporter.finish()
+
+ # flush stdout first; if they're directing it all to a file, this makes
+ # results not get the final message shoved in midway
+ out.stream.flush()
+ return 0
+
+
+replay = subparsers.add_parser(
+ 'replay',
+ description='replay results streams',
+ docs="""
+ Replay previous results streams from pkgcheck, feeding the results into
+ a reporter. Currently only supports replaying streams from
+ pickled-based reporters.
+
+ Useful if you need to delay acting on results until it can be done in
+ one minimal window (say updating a database), or want to generate
+ several different reports without using a config defined multiplex
+ reporter.
+ """)
+replay.add_argument(
+ dest='pickle_file', type=argparse.FileType(), help='pickled results file')
+replay.add_argument(
+ dest='reporter', help='python namespace path reporter to replay it into')
+replay.add_argument(
+ '--out', default=None, help='redirect reporters output to a file')
+@replay.bind_final_check
+def _replay_validate_args(parser, namespace):
+ func = namespace.config.pkgcheck_reporter_factory.get(namespace.reporter)
+ if func is None:
+ func = list(base.Whitelist([namespace.reporter]).filter(
+ get_plugins('reporter', plugins)))
+ if not func:
+ parser.error(
+ "no reporter matches %r (available: %s)" % (
+ namespace.reporter,
+ ', '.join(sorted(x.__name__ for x in get_plugins('reporter', plugins)))
+ )
+ )
+ elif len(func) > 1:
+ parser.error(
+ "--reporter %r matched multiple reporters, "
+ "must match one. %r" % (
+ namespace.reporter,
+ tuple(sorted("%s.%s" % (x.__module__, x.__name__) for x in func))
+ )
+ )
+ func = func[0]
+ namespace.reporter = func
+
+
+def replay_stream(stream_handle, reporter, debug=None):
+ headers = []
+ last_count = 0
+ for count, item in enumerate(pickling.iter_stream(stream_handle)):
+ if isinstance(item, base.StreamHeader):
+ if debug:
+ if headers:
+ debug.write("finished processing %i results for %s" %
+ (count - last_count, headers[-1].criteria))
+ last_count = count
+ debug.write("encountered new stream header for %s" %
+ item.criteria)
+ if headers:
+ reporter.end_check()
+ reporter.start_check(item.checks, item.criteria)
+ headers.append(item)
+ continue
+ reporter.add_report(item)
+ if headers:
+ reporter.end_check()
+ if debug:
+ debug.write(
+ "finished processing %i results for %s" %
+ (count - last_count, headers[-1].criteria))
+
+
+@replay.bind_main_func
+def _replay(options, out, err):
+ if options.out:
+ out = formatters.get_formatter(open(options.out, 'w'))
+ debug = None
+ if options.debug:
+ debug = err
+ replay_stream(options.pickle_file, options.reporter(out), debug=debug)
+ return 0
+
+
def dump_docstring(out, obj, prefix=None):
if prefix is not None:
out.first_prefix.append(prefix)
@@ -560,126 +725,47 @@ def display_reporters(out, options, config_reporters, plugin_reporters):
out.write()
-@argparser.bind_main_func
-def main(options, out, err):
- """Do stuff."""
+show = subparsers.add_parser('show', description='show various pkgcheck info')
+list_options = show.add_argument_group('list options')
+list_options.add_argument(
+ '--keywords', action='store_true', default=False,
+ help='show available warning/error keywords',
+ docs="""
+ List all available keywords.
+
+ Use -v/--verbose to show keywords sorted into the scope they run at
+ (repository, category, package, or version) along with their
+ descriptions.
+ """)
+list_options.add_argument(
+ '--checks', action='store_true', default=False,
+ help='show available checks',
+ docs="""
+ List all available checks.
+
+ Use -v/--verbose to show descriptions and possible keyword results for
+ each check.
+ """)
+list_options.add_argument(
+ '--reporters', action='store_true', default=False,
+ help='show available reporters',
+ docs="""
+ List all available reporters.
- if options.list_keywords:
+ Use -v/--verbose to show reporter descriptions.
+ """)
+@show.bind_main_func
+def _main(options, out, err):
+ if options.keywords:
display_keywords(out, options)
- return 0
- if options.list_checks:
+ if options.checks:
display_checks(out, options)
- return 0
- if options.list_reporters:
+ if options.reporters:
display_reporters(
out, options,
options.config.pkgcheck_reporter_factory.values(),
list(get_plugins('reporter', plugins)))
- return 0
-
- if not options.repo_bases:
- err.write(
- 'Warning: could not determine repo base for profiles, some checks will not work.')
- err.write()
-
- if options.guessed_suite:
- if options.default_suite:
- err.write('Tried to guess a suite to use but got multiple matches')
- err.write('and fell back to the default.')
- else:
- err.write('using suite guessed from working directory')
-
- if options.guessed_target_repo:
- err.write('using repository guessed from working directory')
-
- try:
- reporter = options.reporter(
- out, keywords=options.enabled_keywords, verbose=options.verbose)
- except errors.ReporterInitError as e:
- err.write(
- err.fg('red'), err.bold, '!!! ', err.reset,
- 'Error initializing reporter: ', e)
- return 1
-
- addons_map = {}
-
- def init_addon(klass):
- res = addons_map.get(klass)
- if res is not None:
- return res
- deps = list(init_addon(dep) for dep in klass.required_addons)
- try:
- res = addons_map[klass] = klass(options, *deps)
- except KeyboardInterrupt:
- raise
- except Exception:
- if options.debug:
- err.write('instantiating %s' % (klass,))
- raise
- return res
-
- for addon in options.addons:
- # Ignore the return value, we just need to populate addons_map.
- init_addon(addon)
-
- if options.verbose:
- err.write("target repo: '%s' at '%s'" % (
- options.target_repo.repo_id, options.target_repo.location))
- err.write('base dirs: ', ', '.join(options.repo_bases))
- for filterer in options.limiters:
- err.write('limiter: ', filterer)
- debug = logging.debug
- else:
- debug = None
-
- transforms = list(get_plugins('transform', plugins))
- # XXX this is pretty horrible.
- sinks = list(addon for addon in addons_map.itervalues()
- if getattr(addon, 'feed_type', False))
-
- reporter.start()
-
- for filterer in options.limiters:
- sources = [feeds.RestrictedRepoSource(options.target_repo, filterer)]
- bad_sinks, pipes = base.plug(sinks, transforms, sources, debug)
- if bad_sinks:
- # We want to report the ones that would work if this was a
- # full repo scan separately from the ones that are
- # actually missing transforms.
- bad_sinks = set(bad_sinks)
- full_scope = feeds.RestrictedRepoSource(
- options.target_repo, packages.AlwaysTrue)
- really_bad, ignored = base.plug(sinks, transforms, [full_scope])
- really_bad = set(really_bad)
- assert bad_sinks >= really_bad, \
- '%r unreachable with no limiters but reachable with?' % (
- really_bad - bad_sinks,)
- for sink in really_bad:
- err.error(
- 'sink %s could not be connected (missing transforms?)' % (
- sink,))
- out_of_scope = bad_sinks - really_bad
- if options.verbose and out_of_scope:
- err.warn('skipping repo checks (not a full repo scan)')
- if not pipes:
- out.write(out.fg('red'), ' * ', out.reset, 'No checks!')
- else:
- if options.debug:
- err.write('Running %i tests' % (len(sinks) - len(bad_sinks),))
- for source, pipe in pipes:
- pipe.start()
- reporter.start_check(
- list(base.collect_checks_classes(pipe)), filterer)
- for thing in source.feed():
- pipe.feed(thing, reporter)
- pipe.finish(reporter)
- reporter.end_check()
-
- reporter.finish()
- # flush stdout first; if they're directing it all to a file, this makes
- # results not get the final message shoved in midway
- out.stream.flush()
return 0
diff --git a/pkgcheck/test/test_pkgcheck.py b/pkgcheck/test/test_pkgcheck.py
index 7929b6c4..21407be2 100644
--- a/pkgcheck/test/test_pkgcheck.py
+++ b/pkgcheck/test/test_pkgcheck.py
@@ -6,7 +6,7 @@ from pkgcheck.scripts import pkgcheck
class CommandlineTest(TestCase, helpers.ArgParseMixin):
- _argparser = pkgcheck.argparser
+ _argparser = pkgcheck.scan
def test_parser(self):
self.assertError(