Commits

Chris Jerdonek  committed 6e2e5ad Draft

Issue #15302: Switch regrtest from using getopt to using argparse.

This is the first step in refactoring regrtest to use argparse. The
regrtest module's main() function still expects a getopt-style return
value rather than an argparse.Namespace instance.

  • Participants
  • Parent commits 2c33995

Comments (0)

Files changed (3)

File Lib/test/regrtest.py

 #! /usr/bin/env python3
 
 """
-Usage:
+Script to run Python regression tests.
 
+Run this script with -h or --help for documentation.
+"""
+
+USAGE = """\
 python -m test [options] [test_name1 [test_name2 ...]]
 python path/to/Lib/test/regrtest.py [options] [test_name1 [test_name2 ...]]
+"""
 
+DESCRIPTION = """\
+Run Python regression tests.
 
 If no arguments or options are provided, finds all files matching
 the pattern "test_*" in the Lib/test subdirectory and runs
 command line:
 
 python -E -Wd -m test [options] [test_name1 ...]
+"""
 
-
-Options:
-
--h/--help       -- print this text and exit
---timeout TIMEOUT
-                -- dump the traceback and exit if a test takes more
-                   than TIMEOUT seconds; disabled if TIMEOUT is negative
-                   or equals to zero
---wait          -- wait for user input, e.g., allow a debugger to be attached
-
-Verbosity
-
--v/--verbose    -- run tests in verbose mode with output to stdout
--w/--verbose2   -- re-run failed tests in verbose mode
--W/--verbose3   -- display test output on failure
--d/--debug      -- print traceback for failed tests
--q/--quiet      -- no output unless one or more tests fail
--o/--slow       -- print the slowest 10 tests
-   --header     -- print header with interpreter info
-
-Selecting tests
-
--r/--randomize  -- randomize test execution order (see below)
-   --randseed   -- pass a random seed to reproduce a previous random run
--f/--fromfile   -- read names of tests to run from a file (see below)
--x/--exclude    -- arguments are tests to *exclude*
--s/--single     -- single step through a set of tests (see below)
--m/--match PAT  -- match test cases and methods with glob pattern PAT
--G/--failfast   -- fail as soon as a test fails (only with -v or -W)
--u/--use RES1,RES2,...
-                -- specify which special resource intensive tests to run
--M/--memlimit LIMIT
-                -- run very large memory-consuming tests
-   --testdir DIR
-                -- execute test files in the specified directory (instead
-                   of the Python stdlib test suite)
-
-Special runs
-
--l/--findleaks  -- if GC is available detect tests that leak memory
--L/--runleaks   -- run the leaks(1) command just before exit
--R/--huntrleaks RUNCOUNTS
-                -- search for reference leaks (needs debug build, v. slow)
--j/--multiprocess PROCESSES
-                -- run PROCESSES processes at once
--T/--coverage   -- turn on code coverage tracing using the trace module
--D/--coverdir DIRECTORY
-                -- Directory where coverage files are put
--N/--nocoverdir -- Put coverage files alongside modules
--t/--threshold THRESHOLD
-                -- call gc.set_threshold(THRESHOLD)
--n/--nowindows  -- suppress error message boxes on Windows
--F/--forever    -- run the specified tests in a loop, until an error happens
-
-
-Additional Option Details:
+EPILOG = """\
+Additional option details:
 
 -r randomizes test execution order. You can use --randseed=int to provide a
 int seed value for the randomizer; this is useful for reproducing troublesome
 # We import importlib *ASAP* in order to test #15386
 import importlib
 
+import argparse
 import builtins
 import faulthandler
-import getopt
 import io
 import json
 import logging
 
 TEMPDIR = os.path.abspath(tempfile.gettempdir())
 
-def usage(msg):
-    print(msg, file=sys.stderr)
-    print("Use --help for usage", file=sys.stderr)
-    sys.exit(2)
+def _create_parser():
+    # Set prog to prevent the uninformative "__main__.py" from displaying in
+    # error messages when using "python -m test ...".
+    parser = argparse.ArgumentParser(prog='regrtest.py',
+                                     usage=USAGE,
+                                     description=DESCRIPTION,
+                                     epilog=EPILOG,
+                                     add_help=False,
+                                     formatter_class=
+                                       argparse.RawDescriptionHelpFormatter)
+
+    # Arguments with this clause added to its help are described further in
+    # the epilog's "Additional option details" section.
+    more_details = '  See the section at bottom for more details.'
+
+    group = parser.add_argument_group('General options')
+    # We add help explicitly to control what argument group it renders under.
+    group.add_argument('-h', '--help', action='help',
+                       help='show this help message and exit')
+    group.add_argument('--timeout', metavar='TIMEOUT',
+                        help='dump the traceback and exit if a test takes '
+                             'more than TIMEOUT seconds; disabled if TIMEOUT '
+                             'is negative or equals to zero')
+    group.add_argument('--wait', action='store_true', help='wait for user '
+                        'input, e.g., allow a debugger to be attached')
+    group.add_argument('--slaveargs', metavar='ARGS')
+    group.add_argument('-S', '--start', metavar='START', help='the name of '
+                        'the test at which to start.' + more_details)
+
+    group = parser.add_argument_group('Verbosity')
+    group.add_argument('-v', '--verbose', action='store_true',
+                       help='run tests in verbose mode with output to stdout')
+    group.add_argument('-w', '--verbose2', action='store_true',
+                       help='re-run failed tests in verbose mode')
+    group.add_argument('-W', '--verbose3', action='store_true',
+                       help='display test output on failure')
+    group.add_argument('-d', '--debug', action='store_true',
+                       help='print traceback for failed tests')
+    group.add_argument('-q', '--quiet', action='store_true',
+                       help='no output unless one or more tests fail')
+    group.add_argument('-o', '--slow', action='store_true',
+                       help='print the slowest 10 tests')
+    group.add_argument('--header', action='store_true',
+                       help='print header with interpreter info')
+
+    group = parser.add_argument_group('Selecting tests')
+    group.add_argument('-r', '--randomize', action='store_true',
+                       help='randomize test execution order.' + more_details)
+    group.add_argument('--randseed', metavar='SEED', help='pass a random seed '
+                       'to reproduce a previous random run')
+    group.add_argument('-f', '--fromfile', metavar='FILE', help='read names '
+                       'of tests to run from a file.' + more_details)
+    group.add_argument('-x', '--exclude', action='store_true',
+                       help='arguments are tests to *exclude*')
+    group.add_argument('-s', '--single', action='store_true', help='single '
+                       'step through a set of tests.' + more_details)
+    group.add_argument('-m', '--match', metavar='PAT', help='match test cases '
+                       'and methods with glob pattern PAT')
+    group.add_argument('-G', '--failfast', action='store_true', help='fail as '
+                       'soon as a test fails (only with -v or -W)')
+    group.add_argument('-u', '--use', metavar='RES1,RES2,...', help='specify '
+                       'which special resource intensive tests to run.' +
+                       more_details)
+    group.add_argument('-M', '--memlimit', metavar='LIMIT', help='run very '
+                       'large memory-consuming tests.' + more_details)
+    group.add_argument('--testdir', metavar='DIR',
+                       help='execute test files in the specified directory '
+                            '(instead of the Python stdlib test suite)')
+
+    group = parser.add_argument_group('Special runs')
+    group.add_argument('-l', '--findleaks', action='store_true', help='if GC '
+                       'is available detect tests that leak memory')
+    group.add_argument('-L', '--runleaks', action='store_true',
+                       help='run the leaks(1) command just before exit.' +
+                       more_details)
+    group.add_argument('-R', '--huntrleaks', metavar='RUNCOUNTS',
+                       help='search for reference leaks (needs debug build, '
+                            'very slow).' + more_details)
+    group.add_argument('-j', '--multiprocess', metavar='PROCESSES',
+                       help='run PROCESSES processes at once')
+    group.add_argument('-T', '--coverage', action='store_true', help='turn on '
+                       'code coverage tracing using the trace module')
+    group.add_argument('-D', '--coverdir', metavar='DIR',
+                       help='directory where coverage files are put')
+    group.add_argument('-N', '--nocoverdir', action='store_true',
+                       help='put coverage files alongside modules')
+    group.add_argument('-t', '--threshold', metavar='THRESHOLD',
+                       help='call gc.set_threshold(THRESHOLD)')
+    group.add_argument('-n', '--nowindows', action='store_true',
+                       help='suppress error message boxes on Windows')
+    group.add_argument('-F', '--forever', action='store_true',
+                       help='run the specified tests in a loop, until an '
+                            'error happens')
+
+    parser.add_argument('args', nargs=argparse.REMAINDER,
+                        help=argparse.SUPPRESS)
+
+    return parser
+
+def _convert_namespace_to_getopt(ns):
+    """Convert an argparse.Namespace object to a getopt-style (opts, args)."""
+    opts = []
+    args_dict = vars(ns)
+    for key in sorted(args_dict.keys()):
+        if key == 'args':
+            continue
+        val = args_dict[key]
+        # Don't continue if val equals '' because this means an option
+        # accepting a value was provided the empty string.  Such values should
+        # show up in the returned opts list.
+        if val is None or val is False:
+            continue
+        if val is True:
+            # Then an option with action store_true was passed. getopt
+            # includes these with value '' in the opts list.
+            val = ''
+        opts.append(('--' + key, val))
+    return opts, ns.args
+
+# This function has a getopt-style return value because regrtest.main()
+# was originally written using getopt.
+# TODO: switch this to return an argparse.Namespace instance.
+def _parse_args(args=None):
+    """Parse arguments, and return a getopt-style (opts, args).
+
+    This method mimics the return value of getopt.getopt().  In addition,
+    the (option, value) pairs in opts are sorted by option and use the long
+    option string.
+    """
+    parser = _create_parser()
+    ns = parser.parse_args(args=args)
+    return _convert_namespace_to_getopt(ns)
 
 
 def main(tests=None, testdir=None, verbose=0, quiet=False,
     replace_stdout()
 
     support.record_original_stdout(sys.stdout)
-    try:
-        opts, args = getopt.getopt(sys.argv[1:], 'hvqxsoS:rf:lu:t:TD:NLR:FdwWM:nj:Gm:',
-            ['help', 'verbose', 'verbose2', 'verbose3', 'quiet',
-             'exclude', 'single', 'slow', 'randomize', 'fromfile=', 'findleaks',
-             'use=', 'threshold=', 'coverdir=', 'nocoverdir',
-             'runleaks', 'huntrleaks=', 'memlimit=', 'randseed=',
-             'multiprocess=', 'coverage', 'slaveargs=', 'forever', 'debug',
-             'start=', 'nowindows', 'header', 'testdir=', 'timeout=', 'wait',
-             'failfast', 'match='])
-    except getopt.error as msg:
-        usage(msg)
+
+    opts, args = _parse_args()
 
     # Defaults
     if random_seed is None:
     start = None
     timeout = None
     for o, a in opts:
-        if o in ('-h', '--help'):
-            print(__doc__)
-            return
-        elif o in ('-v', '--verbose'):
+        if o in ('-v', '--verbose'):
             verbose += 1
         elif o in ('-w', '--verbose2'):
             verbose2 = True

File Lib/test/test_regrtest.py

+"""
+Tests of regrtest.py.
+"""
+
+import argparse
+import getopt
+import unittest
+from test import regrtest, support
+
+def old_parse_args(args):
+    """Parse arguments as regrtest did strictly prior to 3.4.
+
+    Raises getopt.GetoptError on bad arguments.
+    """
+    return getopt.getopt(args, 'hvqxsoS:rf:lu:t:TD:NLR:FdwWM:nj:Gm:',
+        ['help', 'verbose', 'verbose2', 'verbose3', 'quiet',
+         'exclude', 'single', 'slow', 'randomize', 'fromfile=', 'findleaks',
+         'use=', 'threshold=', 'coverdir=', 'nocoverdir',
+         'runleaks', 'huntrleaks=', 'memlimit=', 'randseed=',
+         'multiprocess=', 'coverage', 'slaveargs=', 'forever', 'debug',
+         'start=', 'nowindows', 'header', 'testdir=', 'timeout=', 'wait',
+         'failfast', 'match='])
+
+class ParseArgsTestCase(unittest.TestCase):
+
+    """Test that regrtest._parse_args() matches the prior getopt behavior."""
+
+    def _parse_args(self, args):
+        return regrtest._parse_args(args=args)
+
+    def _check_args(self, args, expected=None):
+        """
+        The expected parameter is for cases when the behavior of the new
+        parse_args differs from the old (but deliberately so).
+        """
+        if expected is None:
+            try:
+                expected = old_parse_args(args)
+            except getopt.GetoptError:
+                # Suppress usage string output when an argparse.ArgumentError
+                # error is raised.
+                with support.captured_stderr():
+                    self.assertRaises(SystemExit, self._parse_args, args)
+                return
+        # The new parse_args() sorts by long option string.
+        expected[0].sort()
+        actual = self._parse_args(args)
+        self.assertEqual(actual, expected)
+
+    def test_unrecognized_argument(self):
+        self._check_args(['--xxx'])
+
+    def test_value_not_provided(self):
+        self._check_args(['--start'])
+
+    def test_short_option(self):
+        # getopt returns the short option whereas argparse returns the long.
+        expected = ([('--quiet', '')], [])
+        self._check_args(['-q'], expected=expected)
+
+    def test_long_option(self):
+        self._check_args(['--quiet'])
+
+    def test_long_option__partial(self):
+        self._check_args(['--qui'])
+
+    def test_two_options(self):
+        self._check_args(['--quiet', '--exclude'])
+
+    def test_option_with_value(self):
+        self._check_args(['--start', 'foo'])
+
+    def test_option_with_empty_string_value(self):
+        self._check_args(['--start', ''])
+
+    def test_arg(self):
+        self._check_args(['foo'])
+
+    def test_option_and_arg(self):
+        self._check_args(['--quiet', 'foo'])
+
+    def test_fromfile(self):
+        self._check_args(['--fromfile', 'file'])
+
+    def test_match(self):
+        self._check_args(['--match', 'pattern'])
+
+    def test_randomize(self):
+        self._check_args(['--randomize'])
+
+
+def test_main():
+    support.run_unittest(__name__)
+
+if __name__ == '__main__':
+    test_main()
 - Issue #10646: Tests rearranged for os.samefile/samestat to check for not
   just symlinks but also hard links.
 
+- Issue #15302: Switch regrtest from using getopt to using argparse.
+
 - Issue #15324: Fix regrtest parsing of --fromfile, --match, and --randomize
   options.