blob: 892cdf945127c3b3e64e2f1a6ab4cb29fca15252 [file] [log] [blame]
#!/usr/bin/env python2.7
# Copyright 2019 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import re
import sys
import command
from host import Host
# This file contains the arguments parsing for "fx fuzz".
class ArgParser(argparse.ArgumentParser):
"""Wrapper to ArgumentParser that suppresses help and usage messages.
argparse adds a lot of benefit, but its help and usage messages don't fit
well with the style of other "fx" utilities. As a result, this class
overrides the error() and exit() methods to suppress printing those
messages. As an added bonus, the incorporation of Host
makes it possible to test these conditions.
Attributes:
host: The host object representing the current system.
"""
# These are used in parse_args() to identify and extract libFuzzer arguments.
LIBFUZZER_OPT_RE = re.compile(r'^-(\w+)=(.*)$')
SHORT_OPT_RE = re.compile(r'^-\w$')
LONG_OPT_RE = re.compile(r'^--\w+$')
POSITIONAL_RE = re.compile(r'^[^-]')
def __init__(self, **kwargs):
# Don't use argparse's built-in help.
kwargs['usage'] = ''
kwargs['add_help'] = False
super(ArgParser, self).__init__(**kwargs)
self._parsers = {}
self._unique_options = []
self._options_help = []
self._arguments_help = []
self._has_libfuzzer_extras = False
@property
def host(self):
"""The system interface for user interactions."""
assert self._host, 'Host not set.'
return self._host
@host.setter
def host(self, host):
self._host = host
def add_parsers(self):
"""Configure a top-level parser with subparsers.
Each subcommand should add a parser using the special SubparserAction
returned by argparse.ArgumentParser.add_subparsers(). See
https://docs.python.org/2/library/argparse.html#sub-commands.
"""
self.usage = '[SUBCOMMAND] [...]'
self.description = [
'Manage Fuchsia fuzzers. SUBCOMMAND defaults to "start" if omitted.'
]
# fx fuzz help
help_parser = self._add_parser('help')
help_parser.help = 'Print this message and exit.'
help_parser.description = [
'Prints the detailed help for SUBCOMMAND if provided, or a general help message.'
]
help_parser._add_argument(
'subcommand',
nargs='?',
help=['Subcommand for which to print detailed help.'])
# fx fuzz list [name]
list_parser = self._add_parser('list')
list_parser.help = 'List available fuzzers in the current build.'
list_parser.description = [
'Lists fuzzers matching NAME if provided, or all fuzzers.'
]
list_parser._add_verbose_flag()
list_parser._add_name_argument(required=False)
list_parser.set_defaults(command=command.list_fuzzers)
# fx fuzz start [-d] [-f] [-m] [-o <output>] <name>
start_parser = self._add_parser('start')
start_parser.help = 'Start a specific fuzzer.'
start_parser.description = ['Starts the named fuzzer.']
start_parser._add_debug_flag()
start_parser._add_flag(
'-f', '--foreground', help=['Display fuzzer output.'])
start_parser._add_flag('-m', '--monitor')
start_parser._add_output_option()
start_parser._add_verbose_flag()
start_parser._add_name_argument(required=True)
start_parser._add_libfuzzer_extras()
start_parser.set_defaults(command=command.start_fuzzer)
# fx fuzz check [name]
check_parser = self._add_parser('check')
check_parser.description = [
'Reports status for the fuzzer matching NAME if provided, or for all running',
'fuzzers. Status includes execution state, corpus size, and number of artifacts.',
]
check_parser.help = 'Check on the status of one or more fuzzers.'
check_parser._add_verbose_flag()
check_parser._add_name_argument(required=False)
check_parser.set_defaults(command=command.check_fuzzer)
# fx fuzz stop <name>
stop_parser = self._add_parser('stop')
stop_parser.description = ['Stops the named fuzzer.']
stop_parser.help = 'Stop a specific fuzzer.'
stop_parser._add_verbose_flag()
stop_parser._add_name_argument(required=True)
stop_parser.set_defaults(command=command.stop_fuzzer)
# fx fuzz repro [-d] [-o <output>] <name> <file>...
repro_parser = self._add_parser('repro')
repro_parser.description = [
'Runs the named fuzzer on provided test units.'
]
repro_parser.help = 'Reproduce fuzzer findings by replaying test units.'
repro_parser._add_debug_flag()
repro_parser._add_output_option()
repro_parser._add_verbose_flag()
repro_parser._add_name_argument(required=True)
repro_parser._add_argument(
'libfuzzer_inputs',
metavar='unit',
nargs='+',
help=[
'File containing a fuzzer input, such as an artifact from a',
'previous fuzzer run. Artifacts are typically named by the',
'type of artifact and a digest of the fuzzer input, e.g.',
'crash-2c5d0d1831b242b107a4c42bba2fa3f6d85edc35',
])
repro_parser._add_libfuzzer_extras()
repro_parser.set_defaults(command=command.repro_units)
# fx fuzz analyze [-c <dir>] [-d <file>] [-l] [-o <output>] <name>
analyze_parser = self._add_parser('analyze')
analyze_parser.description = [
'Analyze the corpus and/or dictionary for the given fuzzer.'
]
analyze_parser.help = 'Report coverage info for a given corpus and/or dictionary.'
analyze_parser.add_option(
'-c',
'--corpus',
dest='corpora',
help=['Path to additional corpus elements. May be repeated.'])
analyze_parser.add_option(
'-d',
'--dict',
unique=True,
help=['Path to a fuzzer dictionary. Replaces the package default.'])
analyze_parser._add_flag(
'-l', '--local', help=['Exclude corpus elements from Clusterfuzz.'])
analyze_parser._add_output_option()
analyze_parser._add_verbose_flag()
analyze_parser._add_name_argument(required=True)
analyze_parser._add_libfuzzer_extras()
analyze_parser.set_defaults(command=command.analyze_fuzzer)
update_parser = self._add_parser('update')
update_parser.description = [
'Update the BUILD.gn file for a fuzzer corpus.'
]
update_parser.help = 'Update the BUILD.gn file for a fuzzer corpus.'
update_parser._add_output_option()
update_parser._add_verbose_flag()
update_parser._add_name_argument(required=True)
update_parser.set_defaults(command=command.update_corpus)
unittest_parser = self._add_parser('unittest')
unittest_parser.description = [
'Run the unittests for this tool. This runs all tests from all test cases. To run',
'a single test, use "python <path/to/test.py> <test_name>" instead.'
]
unittest_parser._add_verbose_flag()
unittest_parser.help = 'Run the unittests for this tool.'
unittest_parser.set_defaults(command=command.run_unittests)
e2e_test_parser = self._add_parser('e2etest')
e2e_test_parser.description = [
'Run the end-to-end test for this tool. If a fuzzer is named, it',
'will be run. If none is specified, several example fuzzers will be',
'used. This requires the fuzzer(s) to have already been built and',
'deployed to a running device.'
]
e2e_test_parser._add_flag(
'-l', '--local', help=['Exclude corpus elements from Clusterfuzz.'])
e2e_test_parser._add_verbose_flag()
e2e_test_parser._add_name_argument(required=False)
e2e_test_parser.help = 'Run the end-to-end test for this tool.'
e2e_test_parser.set_defaults(command=command.run_e2e_test)
# TODO(fxb/24828) Once this is ready, merge with analyze or another tool
coverage_parser = self._add_parser('coverage')
coverage_parser.description = [
'[EXPERIMENTAL] Generates a coverage report for a set of tests.',
'Requires --variant profile to be set via fx set to generate the',
'necessary symbols. It is suggested to run with --no-goma in order',
'to preserve linking to files in the report.',
]
coverage_parser._add_verbose_flag()
coverage_parser._add_name_argument(required=True)
coverage_parser._add_output_option()
coverage_parser.help = 'Generate a coverage report for a test.'
coverage_parser.set_defaults(command=command.measure_coverage)
self.epilog = [
'See "fx fuzz help [SUBCOMMAND]" for details on each subcommand.',
'See also "fx help fuzz" for global "fx" options.',
'See https://fuchsia.dev/fuchsia-src/development/testing/fuzzing/libfuzzer',
'for details on writing and building fuzzers.',
]
def _add_parser(self, subcommand):
"""Return a subparser for a specific subcommand."""
parser = ArgParser(prog=subcommand)
parser.host = self.host
self._parsers[subcommand] = parser
return parser
def _format_help(self, item, help_msg):
"""Format a help message for an argument or option.
If help_msg is None, the item will be suppressed from the help entirely.
"""
lines = []
if help_msg:
lines += [' {}'.format(item).ljust(22) + help_msg[0]]
lines += [(' ' * 22) + line for line in help_msg[1:]]
return lines
def _add_flag(self, short_opt, long_opt, **kwargs):
"""Add an optional command line boolean flag.
If help_msg is None, the option is suppressed from the help message.
"""
help_msg = kwargs.pop('help', None)
item = '{},{}'.format(short_opt, long_opt)
self._options_help += self._format_help(item, help_msg)
kwargs['action'] = 'store_true'
self.add_argument(short_opt, long_opt, **kwargs)
def add_option(self, short_opt, long_opt, **kwargs):
"""Add an command line option that takes a parameter.
If unique is False, the option may be repeated.
If help_msg is None, the option is suppressed from the help message.
"""
help_msg = kwargs.pop('help', None)
metavar = kwargs.pop('metavar', long_opt[2:].upper())
unique = kwargs.pop('unique', False)
item = '{},{} {}'.format(short_opt, long_opt, metavar)
self._options_help += self._format_help(item, help_msg)
if unique:
self._unique_options.append(long_opt[2:])
kwargs['action'] = 'append'
self.add_argument(short_opt, long_opt, **kwargs)
def _add_argument(self, name, **kwargs):
"""Add a positional command line argument.
If help is None, the argument is suppressed from the help message.
"""
help_msg = kwargs.pop('help', None)
metavar = kwargs.pop('metavar', name).upper()
nargs = kwargs.get('nargs', None)
usage = metavar
if nargs == '+' or nargs == '*':
usage = '{}...'.format(usage)
if nargs == '?' or nargs == '*':
usage = '[{}]'.format(usage)
self.usage += ' {}'.format(usage)
self._arguments_help += self._format_help(metavar, help_msg)
self.add_argument(name, **kwargs)
def _add_name_argument(self, required=False):
"""Adds a fuzzer "NAME" argument.
If required is True, a fuzzer name must be supplied.
"""
self._add_argument(
'name',
nargs=None if required else '?',
help=[
'Fuzzer name to match. This can be part of the package',
'and/or target name, e.g. "foo", "bar", and "foo/bar" all',
'match "foo_package/bar_target".',
])
def _add_debug_flag(self):
"""Adds a "--debug" flag."""
self._add_flag(
'-g',
'--debug',
help=['Disable exception handling so a debugger can be attached'])
def _add_output_option(self):
"""Adds an "--output OUTPUT" option."""
self.add_option(
'-o',
'--output',
unique=True,
help=['Path under which to store results.'])
def _add_verbose_flag(self):
"""Adds a "--verbose" flag."""
self._add_flag('-v', '--verbose', help=['Display additional output.'])
def _add_libfuzzer_extras(self):
"""Adds liFuzzer options and subprocess arguments.
If a name is given, it will be listed as an argument.
"""
if not self._has_libfuzzer_extras:
self._has_libfuzzer_extras = True
self.usage += ' [...]'
self.epilog = [
'Additional options and/or arguments are passed through to libFuzzer.',
'See https://llvm.org/docs/LibFuzzer.html for details.',
]
def parse_args(self, args=None):
"""Parse args either as a top-level parser or subcommand."""
if args == None:
args = sys.argv[1:]
if self._parsers:
return self._dispatch_to_subcommand(args)
else:
return self._parse_subcommand_args(args)
def _dispatch_to_subcommand(self, args):
"""Invokes the correct subcommand subparser."""
if not args or args[0] in ['-h', '--help']:
subcommand = 'help'
args = []
elif args[0] not in self._parsers:
subcommand = 'start'
else:
subcommand = args[0]
args = args[1:]
self.host.trace('fx fuzz {} {}'.format(subcommand, ' '.join(args)))
args = self._parsers[subcommand].parse_args(args)
if subcommand != 'help':
return args
if not args.subcommand:
self.host.echo(*self.generate_help())
elif args.subcommand in self._parsers:
self.host.echo(*self._parsers[args.subcommand].generate_help())
else:
self.error('Unrecognized subcommand: "{}".'.format(args.subcommand))
self.exit()
def _parse_subcommand_args(self, args):
"""Extract libFuzzer arguments and parse the rest.
This method extracts the libfuzzer options (of the form "-key=val") and
the subprocess arguments (which follow "--") to avoid them getting
mangled by argparse. The positional libFuzzer inputs are collected as
expected, see _add_libfuzzer_inputs().
"""
if self._has_libfuzzer_extras:
libfuzzer_opts = {}
pass_to_subprocess = False
subprocess_args = []
valid = []
for arg in args:
libfuzzer_opt = ArgParser.LIBFUZZER_OPT_RE.match(arg)
if pass_to_subprocess:
subprocess_args.append(arg)
elif arg == '--':
pass_to_subprocess = True
elif libfuzzer_opt:
libfuzzer_opts[libfuzzer_opt.group(
1)] = libfuzzer_opt.group(2)
elif (ArgParser.SHORT_OPT_RE.match(arg) or
ArgParser.LONG_OPT_RE.match(arg) or
ArgParser.POSITIONAL_RE.match(arg)):
valid.append(arg)
else:
self.error('Unrecognized option: {}'.format(arg))
args = valid
# Parse!
args = super(ArgParser, self).parse_args(args)
# Check for options that were incorrectly repeated
for key in self._unique_options:
val = getattr(args, key, None)
if not val:
continue
if len(val) == 1:
setattr(args, key, val[0])
else:
self.error('Repeated option: {}'.format(key))
# Forward libFuzzer arguments
if self._has_libfuzzer_extras:
args.libfuzzer_opts = libfuzzer_opts
args.subprocess_args = subprocess_args
return args
def generate_help(self):
"""Builds the help message as a list of lines.
Example output:
Usage: fx fuzz foobar [OPTIONS] NAME [...]
Arguments:
NAME The thing it's called
Options:
--foo BAR A baz, and then some.
"""
lines = ['']
usage = 'Usage: fx fuzz '
if not self._parsers:
usage += self.prog
if self._options_help:
usage += ' [OPTIONS]'
usage += self.usage
lines += [usage]
if self.description:
lines += ['']
lines += self.description
if self._parsers:
lines += ['']
lines += ['Subcommands:']
for prog, subcommand in sorted(self._parsers.iteritems()):
lines += self._format_help(prog, [subcommand.help])
if self._arguments_help:
lines += ['']
lines += ['Arguments:']
lines += self._arguments_help
if self._options_help:
lines += ['']
lines += ['Options:']
lines += self._options_help
if self.epilog:
lines += ['']
lines += self.epilog
lines += ['']
for line in lines:
assert len(line) <= 80, 'Line is too long:\n"{}"'.format(line)
return lines
def exit(self, status=0, message=None):
if message:
self.host.error(message, 'Try "fx fuzz help".')
sys.exit(status)
def error(self, message):
"""Prints an error message and exits.
Cleans up the argparse messages before passing them to exit, e.g.
argparse's messages are lowercase and unpunctuated.
"""
if ':' not in message and not message.endswith('.'):
message += '.'
self.exit(2, message.capitalize())