[Zope3-checkins] SVN: zope.testing/trunk/src/zope/testing/testrunner Merge tim-pyc branch.

Tim Peters tim.one at comcast.net
Wed Sep 28 18:15:02 EDT 2005


Log message for revision 38672:
  Merge tim-pyc branch.
  
  Adds a new `--usecompiled` option.  When specified, tests will run even if
  .py source files are missing, provided the appropriate .pyc or .pyo files
  exist.
  
  Sped find_test_files() by hoisting an invariant test out of a loop.
  
  Miscellaneous whitespace and typo repairs.
  

Changed:
  U   zope.testing/trunk/src/zope/testing/testrunner-edge-cases.txt
  A   zope.testing/trunk/src/zope/testing/testrunner-ex/usecompiled/
  _U  zope.testing/trunk/src/zope/testing/testrunner-ex/usecompiled/README.txt
  _U  zope.testing/trunk/src/zope/testing/testrunner-ex/usecompiled/__init__.py
  _U  zope.testing/trunk/src/zope/testing/testrunner-ex/usecompiled/compiletest.py
  _U  zope.testing/trunk/src/zope/testing/testrunner-ex/usecompiled/package/__init__.py
  _U  zope.testing/trunk/src/zope/testing/testrunner-ex/usecompiled/package/compiletest.py
  U   zope.testing/trunk/src/zope/testing/testrunner.py
  U   zope.testing/trunk/src/zope/testing/testrunner.txt

-=-
Modified: zope.testing/trunk/src/zope/testing/testrunner-edge-cases.txt
===================================================================
--- zope.testing/trunk/src/zope/testing/testrunner-edge-cases.txt	2005-09-28 21:22:25 UTC (rev 38671)
+++ zope.testing/trunk/src/zope/testing/testrunner-edge-cases.txt	2005-09-28 22:15:02 UTC (rev 38672)
@@ -144,7 +144,7 @@
     <BLANKLINE>
     Error in test test_post_mortem2 (sample3.sampletests_d.TestSomething)
     Traceback (most recent call last):
-      File "testrunner-ex/sample3/sampletests_d.py", 
+      File "testrunner-ex/sample3/sampletests_d.py",
            line 37, in test_post_mortem2
         g()
       File "testrunner-ex/sample3/sampletests_d.py", line 46, in g
@@ -184,7 +184,7 @@
         exc_info)
       File "zope/testing/doctest.py", line 1737, in report_unexpected_exception
         raise UnexpectedException(test, example, exc_info)
-    UnexpectedException: 
+    UnexpectedException:
        from testrunner-ex/sample3/sampletests_d.py:61 (2 examples)>
     <BLANKLINE>
     exceptions.ValueError:
@@ -310,7 +310,7 @@
     <BLANKLINE>
     Error in test post_mortem_failure2 (sample3.sampletests_d)
     <BLANKLINE>
-    File "testrunner-ex/sample3/sampletests_d.py", 
+    File "testrunner-ex/sample3/sampletests_d.py",
                    line 81, in sample3.sampletests_d.post_mortem_failure2
     <BLANKLINE>
     x
@@ -366,7 +366,7 @@
     (Pdb) c
     True
 
-Post-mortem debugging with tripple verbosity
+Post-mortem debugging with triple verbosity
 
     >>> sys.argv = 'test --layer samplelayers.Layer1$ -vvv -D'.split()
     >>> testrunner.run(defaults)

Copied: zope.testing/trunk/src/zope/testing/testrunner-ex/usecompiled (from rev 38671, zope.testing/branches/tim-pyc/src/zope/testing/testrunner-ex/usecompiled)

Modified: zope.testing/trunk/src/zope/testing/testrunner.py
===================================================================
--- zope.testing/trunk/src/zope/testing/testrunner.py	2005-09-28 21:22:25 UTC (rev 38671)
+++ zope.testing/trunk/src/zope/testing/testrunner.py	2005-09-28 22:15:02 UTC (rev 38672)
@@ -69,13 +69,57 @@
             threading.settrace(None)
         self.starte = False
 
-
 class EndRun(Exception):
     """Indicate that the existing run call should stop
 
     Used to prevent additional test output after post-mortem debugging.
     """
 
+def strip_py_ext(options, path):
+    """Return path without its .py (or .pyc or .pyo) extension, or None.
+
+    If options.usecompiled is false:
+        If path ends with ".py", the path without the extension is returned.
+        Else None is returned.
+
+    If options.usecompiled is true:
+        If Python is running with -O, a .pyo extension is also accepted.
+        If Python is running without -O, a .pyc extension is also accepted.
+    """
+    if path.endswith(".py"):
+        return path[:-3]
+    if options.usecompiled:
+        if __debug__:
+            # Python is running without -O.
+            ext = ".pyc"
+        else:
+            # Python is running with -O.
+            ext = ".pyo"
+        if path.endswith(ext):
+            return path[:-len(ext)]
+    return None
+
+def contains_init_py(options, fnamelist):
+    """Return true iff fnamelist contains a suitable spelling of __init__.py.
+
+    If options.usecompiled is false, this is so iff "__init__.py" is in
+    the list.
+
+    If options.usecompiled is true, then "__init__.pyo" is also acceptable
+    if Python is running with -O, and "__init__.pyc" is also acceptable if
+    Python is running without -O.
+    """
+    if "__init__.py" in fnamelist:
+        return True
+    if options.usecompiled:
+        if __debug__:
+            # Python is running without -O.
+            return "__init__.pyc" in fnamelist
+        else:
+            # Python is running with -O.
+            return "__init__.pyo" in fnamelist
+    return False
+
 def run(defaults=None, args=None):
     if args is None:
         args = sys.argv
@@ -707,7 +751,10 @@
         for prefix in options.prefix:
             if fpath.startswith(prefix):
                 # strip prefix, strip .py suffix and convert separator to dots
-                module_name = fpath[len(prefix):-3].replace(os.path.sep, '.')
+                noprefix = fpath[len(prefix):]
+                noext = strip_py_ext(options, noprefix)
+                assert noext is not None
+                module_name = noext.replace(os.path.sep, '.')
                 try:
                     module = import_name(module_name)
                 except:
@@ -737,35 +784,60 @@
 def find_test_files(options):
     found = {}
     for f in find_test_files_(options):
+        if f in found:
+            continue
         for filter in options.module:
             if filter(f):
-                if f not in found:
-                    found[f] = 1
-                    yield f
-                    break
+                found[f] = 1
+                yield f
+                break
 
 identifier = re.compile(r'[_a-zA-Z]\w*$').match
 def find_test_files_(options):
     tests_pattern = options.tests_pattern
     test_file_pattern = options.test_file_pattern
+
+    # If options.usecompiled, we can accept .pyc or .pyo files instead
+    # of .py files.  We'd rather use a .py file if one exists.  `root2ext`
+    # maps a test file path, sans extension, to the path with the best
+    # extension found (.py if it exists, else .pyc or .pyo).
+    # Note that "py" < "pyc" < "pyo", so if more than one extension is
+    # found, the lexicographically smaller one is best.
+
+    # Found a new test file, in directory `dirname`.  `noext` is the
+    # file name without an extension, and `withext` is the file name
+    # with its extension.
+    def update_root2ext(dirname, noext, withext):
+        key = os.path.join(dirname, noext)
+        new = os.path.join(dirname, withext)
+        if key in root2ext:
+            root2ext[key] = min(root2ext[key], new)
+        else:
+            root2ext[key] = new
+
     for p in test_dirs(options, {}):
         for dirname, dirs, files in walk_with_symlinks(options, p):
-            if (dirname != p) and ('__init__.py' not in files):
-                continue
+            if dirname != p and not contains_init_py(options, files):
+                continue    # not a plausible test directory
+            root2ext = {}
             dirs[:] = filter(identifier, dirs)
             d = os.path.split(dirname)[1]
-            if tests_pattern(d) and ('__init__.py' in files):
+            if tests_pattern(d) and contains_init_py(options, files):
                 # tests directory
                 for file in files:
-                    if file.endswith('.py') and test_file_pattern(file[:-3]):
-                        f = os.path.join(dirname, file)
-                        yield f
+                    noext = strip_py_ext(options, file)
+                    if noext and test_file_pattern(noext):
+                        update_root2ext(dirname, noext, file)
 
             for file in files:
-                if file.endswith('.py') and tests_pattern(file[:-3]):
-                    f = os.path.join(dirname, file)
-                    yield f
+                noext = strip_py_ext(options, file)
+                if noext and tests_pattern(noext):
+                    update_root2ext(dirname, noext, file)
 
+            winners = root2ext.values()
+            winners.sort()
+            for file in winners:
+                yield file
 
 def walk_with_symlinks(options, dir):
     # TODO -- really should have test of this that uses symlinks
@@ -1134,6 +1206,19 @@
 the tests, can make the test run go much faster.
 """)
 
+other.add_option(
+    '--usecompiled', action="store_true", dest='usecompiled',
+    help="""\
+Normally, a package must contain an __init__.py file, and only .py files
+can contain test code.  When this option is specified, compiled Python
+files (.pyc and .pyo) can be used instead:  a directory containing
+__init__.pyc or __init__.pyo is also considered to be a package, and if
+file XYZ.py contains tests but is absent while XYZ.pyc or XYZ.pyo exists
+then the compiled files will be used.  This is necessary when running
+tests against a tree where the .py files have been removed after
+compilation to .pyc/.pyo.  Use of this option implies --keepbytecode.
+""")
+
 parser.add_option_group(other)
 
 ######################################################################
@@ -1218,6 +1303,9 @@
 
     options.layer = options.layer and dict([(l, 1) for l in options.layer])
 
+    if options.usecompiled:
+        options.keepbytecode = options.usecompiled
+
     return options
 
 # Command-line UI

Modified: zope.testing/trunk/src/zope/testing/testrunner.txt
===================================================================
--- zope.testing/trunk/src/zope/testing/testrunner.txt	2005-09-28 21:22:25 UTC (rev 38671)
+++ zope.testing/trunk/src/zope/testing/testrunner.txt	2005-09-28 22:15:02 UTC (rev 38672)
@@ -239,8 +239,8 @@
 
 In most of the examples here, we set up `sys.argv`.  In normal usage,
 the testrunner just uses `sys.argv`.  It is possible to pass athiments
-explicitly. 
-    
+explicitly.
+
     >>> testrunner.run(defaults, 'test --layer 111'.split())
     Running samplelayers.Layer111 tests:
       Set up samplelayers.Layerx in N.NNN seconds.
@@ -1395,7 +1395,7 @@
       File "testrunner-ex/sample2/sampletests_1.py", line 17, in eek
     <BLANKLINE>
     ----------------------------------------------------------------------
-    File "testrunner-ex/sample2/sampletests_1.py", line 19, 
+    File "testrunner-ex/sample2/sampletests_1.py", line 19,
          in sample2.sampletests_1.eek
     Failed example:
         x = y
@@ -1407,7 +1407,7 @@
             x = y
         NameError: name 'y' is not defined
     ----------------------------------------------------------------------
-    File "testrunner-ex/sample2/sampletests_1.py", line 21, 
+    File "testrunner-ex/sample2/sampletests_1.py", line 21,
          in sample2.sampletests_1.eek
     Failed example:
         x
@@ -1419,7 +1419,7 @@
             x
         NameError: name 'x' is not defined
     ----------------------------------------------------------------------
-    File "testrunner-ex/sample2/sampletests_1.py", line 24, 
+    File "testrunner-ex/sample2/sampletests_1.py", line 24,
          in sample2.sampletests_1.eek
     Failed example:
         z = x + 1
@@ -1449,7 +1449,7 @@
       File "testrunner-ex/sample2/sampletests_1.py", line 17, in eek
     <BLANKLINE>
     ----------------------------------------------------------------------
-    File "testrunner-ex/sample2/sampletests_1.py", line 19, 
+    File "testrunner-ex/sample2/sampletests_1.py", line 19,
          in sample2.sampletests_1.eek
     Failed example:
         x = y
@@ -1592,7 +1592,7 @@
 break in the pdb.set_trace function.  It was necessary to use 'next'
 or 'up' to get to the application code that called pdb.set_trace.  In
 Python 2.4, pdb.set_trace causes pdb to stop right after the call to
-pdb.set_trace. 
+pdb.set_trace.
 
 You can also do post-mortem debugging, using the --post-mortem (-D)
 option:
@@ -1608,7 +1608,7 @@
     <BLANKLINE>
     Error in test test_post_mortem1 (sample3.sampletests_d.TestSomething)
     Traceback (most recent call last):
-      File "testrunner-ex/sample3/sampletests_d.py", 
+      File "testrunner-ex/sample3/sampletests_d.py",
               line 34, in test_post_mortem1
         raise ValueError
     ValueError
@@ -1926,3 +1926,89 @@
 
     >>> import shutil
     >>> shutil.rmtree('coverage_dir')
+
+Running Without Source Code
+---------------------------
+
+The ``--usecompiled`` option allows running tests in a tree without .py
+source code, provided compiled .pyc or .pyo files exist (without
+``--usecompiled``, .py files are necessary).
+
+We have a very simple directory tree, under ``usecompiled/``, to test
+this.  Because we're going to delete its .py files, we want to work
+in a copy of that:
+
+    >>> NEWNAME = "unlikely_package_name"
+    >>> src = os.path.join(this_directory, 'testrunner-ex', 'usecompiled')
+    >>> os.path.isdir(src)
+    True
+    >>> dst = os.path.join(this_directory, 'testrunner-ex', NEWNAME)
+    >>> os.path.isdir(dst)
+    False
+
+Have to use our own copying code, to avoid copying read-only SVN files that
+can't be deleted later.
+
+    >>> n = len(src) + 1
+    >>> for root, dirs, files in os.walk(src):
+    ...     dirs[:] = [d for d in dirs if d == "package"] # prune cruft
+    ...     os.mkdir(os.path.join(dst, root[n:]))
+    ...     for f in files:
+    ...         shutil.copy(os.path.join(root, f),
+    ...                     os.path.join(dst, root[n:], f))
+
+Now run the tests in the copy:
+
+    >>> mydefaults = [
+    ...     '--path', directory_with_tests,
+    ...     '--tests-pattern', '^compiletest$',
+    ...     '--package', NEWNAME,
+    ...     '-vv',
+    ...     ]
+    >>> sys.argv = ['test']
+    >>> testrunner.run(mydefaults)
+    Running tests at level 1
+    Running unit tests:
+      Running:
+        test1 (unlikely_package_name.compiletest.Test)
+        test2 (unlikely_package_name.compiletest.Test)
+        test1 (unlikely_package_name.package.compiletest.Test)
+        test2 (unlikely_package_name.package.compiletest.Test)
+      Ran 4 tests with 0 failures and 0 errors in N.NNN seconds.
+    False
+
+If we delete the source files, it's normally a disaster:  the test runner
+doesn't believe any test files, or even packages, exist.  Note that we pass
+``--keepbytecode`` this time, because otherwise the test runner would
+delete the compiled Python files too:
+
+    >>> for root, dirs, files in os.walk(dst):
+    ...    dirs[:] = [d for d in dirs if d == "package"] # prune cruft
+    ...    for f in files:
+    ...        if f.endswith(".py"):
+    ...            os.remove(os.path.join(root, f))
+    >>> testrunner.run(mydefaults, ["test", "--keepbytecode"])
+    Running tests at level 1
+    Total: 0 tests, 0 failures, 0 errors
+    False
+
+Finally, passing ``--usecompiled`` asks the test runner to treat .pyc
+and .pyo files as adequate replacements for .py files.  Note that the
+output is the same as when running with .py source above.  The absence
+of "removing stale bytecode ..." messages shows that ``--usecompiled``
+also implies ``--keepbytecode``:
+
+    >>> testrunner.run(mydefaults, ["test", "--usecompiled"])
+    Running tests at level 1
+    Running unit tests:
+      Running:
+        test1 (unlikely_package_name.compiletest.Test)
+        test2 (unlikely_package_name.compiletest.Test)
+        test1 (unlikely_package_name.package.compiletest.Test)
+        test2 (unlikely_package_name.package.compiletest.Test)
+      Ran 4 tests with 0 failures and 0 errors in N.NNN seconds.
+    False
+
+Remove the copy:
+
+    >>> shutil.rmtree(dst)



More information about the Zope3-Checkins mailing list