Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-testflo for openSUSE:Factory 
checked in at 2026-04-18 21:35:14
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-testflo (Old)
 and      /work/SRC/openSUSE:Factory/.python-testflo.new.11940 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-testflo"

Sat Apr 18 21:35:14 2026 rev:12 rq:1347826 version:1.4.22

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-testflo/python-testflo.changes    
2024-01-03 12:24:19.246014512 +0100
+++ /work/SRC/openSUSE:Factory/.python-testflo.new.11940/python-testflo.changes 
2026-04-18 21:35:23.779170440 +0200
@@ -1,0 +2,22 @@
+Fri Apr 17 09:23:17 UTC 2026 - John Paul Adrian Glaubitz 
<[email protected]>
+
+- Update to 1.4.22
+  * Previous release broke for projects that don't have a .coveragerc (#130)
+- from version 1.4.21
+  * Fixed a double initialization issue when coverage was active (#128)
+- from version 1.4.20
+  * Improved functionality of the -f option (#123)
+  * Added a workflow to publish to PyPi when a release is made on GitHub (#122)
+- from version 1.4.19
+  * Added current directory to sys.path during try_import to avoid any
+    relative import failures in test files (#118)
+- from version 1.4.18
+  * Removed setuptools as a dependency (#115)
+- from version 1.4.17
+  * Fixed a couple of bugs related to SubTests (#111)
+- from version 1.4.16
+  * Changed build system to hatchling; added a test workflow (#106)
+  * Fixed handling of --skip_dirs arg (#105)
+- Update BuildRequires from pyproject.toml
+
+-------------------------------------------------------------------

Old:
----
  testflo-1.4.15.tar.gz

New:
----
  testflo-1.4.22.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-testflo.spec ++++++
--- /var/tmp/diff_new_pack.iwmxmJ/_old  2026-04-18 21:35:24.427196851 +0200
+++ /var/tmp/diff_new_pack.iwmxmJ/_new  2026-04-18 21:35:24.435197177 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-testflo
 #
-# Copyright (c) 2024 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,21 +18,21 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-testflo
-Version:        1.4.15
+Version:        1.4.22
 Release:        0
 Summary:        A flow-based testing framework
 License:        Apache-2.0
 Group:          Development/Languages/Python
 URL:            https://github.com/OpenMDAO/testflo
 Source:         
https://files.pythonhosted.org/packages/source/t/testflo/testflo-%{version}.tar.gz
+BuildRequires:  %{python_module hatchling}
 BuildRequires:  %{python_module pip}
-BuildRequires:  %{python_module setuptools}
 BuildRequires:  %{python_module wheel}
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
 Requires:       python-setuptools
 Requires(post): update-alternatives
-Requires(preun):update-alternatives
+Requires(preun): update-alternatives
 Recommends:     python-coverage
 Recommends:     python-mpi4py
 Recommends:     python-psutil

++++++ testflo-1.4.15.tar.gz -> testflo-1.4.22.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/.gitignore 
new/testflo-1.4.22/.gitignore
--- old/testflo-1.4.15/.gitignore       1970-01-01 01:00:00.000000000 +0100
+++ new/testflo-1.4.22/.gitignore       2020-02-02 01:00:00.000000000 +0100
@@ -0,0 +1,2 @@
+*.pyc
+*.egg-info
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/DESIGN.txt 
new/testflo-1.4.22/DESIGN.txt
--- old/testflo-1.4.15/DESIGN.txt       2021-12-12 23:38:56.000000000 +0100
+++ new/testflo-1.4.22/DESIGN.txt       1970-01-01 01:00:00.000000000 +0100
@@ -1,43 +0,0 @@
-testflo is a python testing framework that uses a pipeline of
-iterators to process test specifications, run the tests, and process the
-results.
-
-The testflo API consists of a single callable that takes
-an input iterator of Test objects as an argument and returns an
-output iterator of Test objects.  The source of the pipeline is a plain
-python iterator since it doesn't need an input iterator. By simply adding
-members to the testflo pipeline, it's easy to add new features.
-
-The pipeline starts with an iterator of strings that I'll call
-'general test specifiers'.  These can have any of the following forms:
-
-<module or file path>
-<module or file path>:<TestCase class name>.<method name>
-<module or file path>:<function name>
-<directory path>
-
-where <module or file path> is either the filesystem pathname of the
-python file containing the test(s) or the python module path, e.g.,
-'foo.bar.baz'.
-
-The general test specifiers are iterated over by the TestDiscoverer, who
-generates an output iterator of Test objects. There is a Test object for each
-individual test.  As of version 1.1, the objects in the TestDiscoverer's
-output iterator can be either individual Test objects or lists of Test
-objects. This change was necessary to support module level and TestCase
-class level setup and teardown functions.  The thought was that all tests
-under either a module level setup/teardown or a TestCase class level
-setup/teardown should be grouped and executed in the same process, so
-when these functions are present, the Test objects are grouped into a list
-and sent together to the ConcurrentTestRunner.  After execution, the rest
-of the pipeline sees only individual Test objects.
-
-The ConcurrentTestRunner
-executes each test and passes an iterator of those to the ResultPrinter,
-who then passes them on to the ResultSummary.
-
-The multiprocessing library is used in the ConcurrentTestRunner to support 
concurrent
-execution of tests.  It adds Test objects to a shared Queue that the
-worker processes pull from. Then the workers place the finished Test objects in
-a 'done' Queue that the ConcurrentTestRunner pulls from and passes downstream 
for
-display, summary, or whatever.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/MANIFEST.in 
new/testflo-1.4.22/MANIFEST.in
--- old/testflo-1.4.15/MANIFEST.in      2021-12-12 23:38:56.000000000 +0100
+++ new/testflo-1.4.22/MANIFEST.in      1970-01-01 01:00:00.000000000 +0100
@@ -1,4 +0,0 @@
-include README.md
-include LICENSE.txt
-include RELEASE_NOTES.txt
-include DESIGN.txt
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/PKG-INFO new/testflo-1.4.22/PKG-INFO
--- old/testflo-1.4.15/PKG-INFO 2023-12-28 19:25:25.970558400 +0100
+++ new/testflo-1.4.22/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
@@ -1,22 +1,22 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: testflo
-Version: 1.4.15
+Version: 1.4.22
 Summary: A simple flow-based testing framework
-License: Apache 2.0
+License-Expression: Apache-2.0
+License-File: LICENSE.txt
 Classifier: Development Status :: 4 - Beta
 Classifier: License :: OSI Approved :: Apache Software License
 Classifier: Natural Language :: English
 Classifier: Operating System :: MacOS :: MacOS X
-Classifier: Operating System :: POSIX :: Linux
 Classifier: Operating System :: Microsoft :: Windows
+Classifier: Operating System :: POSIX :: Linux
 Classifier: Programming Language :: Python :: 3.5
 Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: Implementation :: CPython
-Description-Content-Type: text/markdown
-License-File: LICENSE.txt
 Requires-Dist: coverage>=6.0
+Description-Content-Type: text/markdown
 
 [![PyPI version][1]][2]
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/pyproject.toml 
new/testflo-1.4.22/pyproject.toml
--- old/testflo-1.4.15/pyproject.toml   1970-01-01 01:00:00.000000000 +0100
+++ new/testflo-1.4.22/pyproject.toml   2020-02-02 01:00:00.000000000 +0100
@@ -0,0 +1,37 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "testflo"
+dynamic = ["version"]
+description = "A simple flow-based testing framework"
+readme = "README.md"
+license = "Apache-2.0"
+classifiers = [
+    "Development Status :: 4 - Beta",
+    "License :: OSI Approved :: Apache Software License",
+    "Natural Language :: English",
+    "Operating System :: MacOS :: MacOS X",
+    "Operating System :: Microsoft :: Windows",
+    "Operating System :: POSIX :: Linux",
+    "Programming Language :: Python :: 3.5",
+    "Programming Language :: Python :: 3.6",
+    "Programming Language :: Python :: 3.7",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: Implementation :: CPython",
+]
+dependencies = [
+    "coverage>=6.0",
+]
+
+[project.scripts]
+testflo = "testflo.main:main"
+
+[tool.hatch.version]
+path = "testflo/__init__.py"
+
+[tool.hatch.build.targets.sdist]
+include = [
+    "/testflo",
+]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/setup.cfg new/testflo-1.4.22/setup.cfg
--- old/testflo-1.4.15/setup.cfg        2023-12-28 19:25:25.970558400 +0100
+++ new/testflo-1.4.22/setup.cfg        1970-01-01 01:00:00.000000000 +0100
@@ -1,7 +0,0 @@
-[metadata]
-description-file = README.md
-
-[egg_info]
-tag_build = 
-tag_date = 0
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/setup.py new/testflo-1.4.22/setup.py
--- old/testflo-1.4.15/setup.py 2023-10-26 20:14:41.000000000 +0200
+++ new/testflo-1.4.22/setup.py 1970-01-01 01:00:00.000000000 +0100
@@ -1,42 +0,0 @@
-from setuptools import setup
-
-import re
-
-from pathlib import Path
-
-__version__ = re.findall(
-    r"""__version__ = ["']+([0-9\.\-dev]*)["']+""",
-    open('testflo/__init__.py').read(),
-)[0]
-
-with open(Path(__file__).parent / "README.md", encoding="utf-8") as f:
-    long_description = f.read()
-
-setup(name='testflo',
-      version=__version__,
-      description="A simple flow-based testing framework",
-      long_description=long_description,
-      long_description_content_type='text/markdown',
-      classifiers=[
-          'Development Status :: 4 - Beta',
-          'License :: OSI Approved :: Apache Software License',
-          'Natural Language :: English',
-          'Operating System :: MacOS :: MacOS X',
-          'Operating System :: POSIX :: Linux',
-          'Operating System :: Microsoft :: Windows',
-          'Programming Language :: Python :: 3.5',
-          'Programming Language :: Python :: 3.6',
-          'Programming Language :: Python :: 3.7',
-          'Programming Language :: Python :: 3.8',
-          'Programming Language :: Python :: Implementation :: CPython',
-      ],
-      license='Apache 2.0',
-      install_requires=[
-        'coverage>=6.0'
-      ],
-      packages=['testflo'],
-      entry_points="""
-          [console_scripts]
-          testflo=testflo.main:main
-      """
-      )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/testflo/__init__.py 
new/testflo-1.4.22/testflo/__init__.py
--- old/testflo-1.4.15/testflo/__init__.py      2023-12-28 19:24:57.000000000 
+0100
+++ new/testflo-1.4.22/testflo/__init__.py      2020-02-02 01:00:00.000000000 
+0100
@@ -1 +1 @@
-__version__ = '1.4.15'
+__version__ = '1.4.22'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/testflo/cover.py 
new/testflo-1.4.22/testflo/cover.py
--- old/testflo-1.4.15/testflo/cover.py 2023-10-26 20:14:41.000000000 +0200
+++ new/testflo-1.4.22/testflo/cover.py 2020-02-02 01:00:00.000000000 +0100
@@ -3,146 +3,103 @@
 """
 import os
 import sys
-import shutil
 import webbrowser
 
 try:
     import coverage
-    from coverage.config import HandyConfigParser
+    from coverage.collector import Collector
 except ImportError:
     coverage = None
-else:
-    coverage.process_startup()
 
 
-# use to hold a global coverage obj
-_coverobj = None
+def setup_coverage(options):
+    """
+    Programmatically initializes coverage for the current process.
+    Ensures absolute paths for temp-dir safety and avoids double-init.
+    """
+    if not (options.coverage or options.coveragehtml):
+        return None
 
-def _to_ini(lst):
-    if lst:
-        return ','.join(lst)
-    return ''
+    # Prevent double-initialization
+    if Collector._collectors:
+        return None
+
+    cover_dir = options.cover_dir or os.getcwd()
+    data_file = os.path.join(cover_dir, '.coverage')
+    cfg_file = os.path.join(cover_dir, '.coveragerc')
+
+    kwargs = {
+        'data_file': data_file,
+        'data_suffix': True,
+        'branch': options.cover_branch,
+    }
 
+    if os.path.isfile(cfg_file):
+        kwargs['config_file'] = cfg_file
 
-def _write_temp_config(options, rcfile):
-    """
-    Read any .coveragerc file if it exists, and override parts of it then 
generate our temp config.
+    cov = coverage.Coverage(**kwargs)
 
-    Parameters
-    ----------
-    options : cmd line options
-        Options from the command line parser.
-    rcfile : str
-        The name of our temporary coverage config file.
-    """
-    tmp_cfg = {
-        'run': {
-            'branch': False,
-            'parallel': True,
-            'concurrency': 'multiprocessing',
-        },
-        'report': {
-            'ignore_errors': True,
-            'skip_empty': True,
-            'sort': '-cover',
-        },
-        'html': {
-            'skip_empty': True,
-        }
-    }
+    cov.config.ignore_errors = True
 
-    if options.coverpkgs:
-        tmp_cfg['run']['source_pkgs'] = _to_ini(options.coverpkgs)
+    if sys.version_info >= (3, 13):
+        cov.set_option("run:core", "sysmon")
 
+    if options.coverpkgs:
+        cov.set_option("run:source", options.coverpkgs)
     if options.cover_omits:
-        tmp_cfg['run']['omit'] = _to_ini(options.cover_omits)
-        tmp_cfg['report']['omit'] = _to_ini(options.cover_omits)
+        omits = cov.get_option("run:omit")
+        if omits:
+            omits.extend(options.cover_omits)
+        else:
+            omits = options.cover_omits
+        cov.set_option("run:omit", omits)
+
+    cov.set_option("run:disable_warnings", ["module-not-imported", 
"no-data-collected",
+                   "couldnt-parse"])
+    cov.set_option("report:ignore_errors", True)
+    cov.set_option("report:sort", "-cover")
+    cov.set_option("report:exclude_lines", [
+        "pragma: no cover",
+        "if __name__ == .__main__.:",
+        "raise NotImplementedError",
+        "def __repr__",
+    ])
+
+    return cov
 
-    cfgparser = HandyConfigParser(our_file=True)
 
-    if os.path.isfile('.coveragerc'):
-        cfgparser.read(['.coveragerc'])
+def finalize_coverage(options, cov):
+    """
+    Combines all parallel coverage files and generates an HTML report.
+    """
+    if cov is None:
+        return
 
-    cfgparser.read_dict(tmp_cfg)
+    cov.save()
 
-    with open(rcfile, 'w') as f:
-        cfgparser.write(f)
+    data_dir = options.cover_dir
 
+    # Combine all coverage files found in the data_dir
+    try:
+        cov.combine(data_paths=[data_dir])
+    except coverage.exceptions.CoverageException as e:
+        print(f"Combining coverage files failed: {e}", file=sys.stderr)
+        return
+
+    if options.coverage:
+        print("\n--- Coverage Summary ---")
+        cov.report(ignore_errors=True, skip_empty=True)
+
+    if options.coveragehtml:
+        html_dir = os.path.join(data_dir, 'htmlcov')
+        cov.html_report(directory=html_dir, ignore_errors=True, 
skip_empty=True,
+                        show_contexts=options.dyn_contexts)
+        index_file = os.path.join(html_dir, 'index.html')
+        if sys.platform == 'darwin':
+            os.system('open %s' % index_file)
+        else:
+            webbrowser.get().open(index_file)
 
-def setup_coverage(options):
-    global _coverobj
-    if _coverobj is None and (options.coverage or options.coveragehtml):
-        if not coverage:
-            raise RuntimeError("coverage has not been installed.")
-        if not options.coverpkgs:
-            raise RuntimeError("No packages specified for coverage. "
-                               "Use the --coverpkg option to add a package.")
-        oldcov = os.path.join(os.getcwd(), '.coverage')
-        if os.path.isfile(oldcov):
-            os.remove(oldcov)
-        covdir = os.path.join(os.getcwd(), '_covdir')
-        if os.path.isdir('_covdir'):
-            shutil.rmtree('_covdir')
-        os.mkdir('_covdir')
-        os.environ['COVERAGE_RUN'] = 'true'
-        os.environ['COVERAGE_RCFILE'] = rcfile = os.path.join(covdir, 
'_coveragerc_')
-        os.environ['COVERAGE_FILE'] = covfile = os.path.join(covdir, 
'.coverage')
-        os.environ['COVERAGE_PROCESS_START'] = rcfile
-        _write_temp_config(options, rcfile)
-        _coverobj = coverage.Coverage(data_file=covfile, data_suffix=True, 
config_file=rcfile)
-    return _coverobj
-
-def start_coverage():
-    if _coverobj:
-        _coverobj.start()
-
-def stop_coverage():
-    if _coverobj:
-        _coverobj.stop()
-
-def save_coverage():
-    if _coverobj:
-        _coverobj.save()
-
-def finalize_coverage(options):
-    if _coverobj and options.coverpkgs:
-        rank = 0
-        if not options.nompi:
-            try:
-                from mpi4py import MPI
-                rank = MPI.COMM_WORLD.rank
-            except ImportError:
-                pass
-        if rank == 0:
-            from testflo.util import find_files, find_module
-            excl = lambda n: (n.startswith('test_') and n.endswith('.py')) or \
-                             n.startswith('__init__.')
-            dirs = []
-            for n in options.coverpkgs:
-                if os.path.isdir(n):
-                    dirs.append(n)
-                else:
-                    path = find_module(n)
-                    if path is None:
-                        raise RuntimeError("Can't find module %s" % n)
-                    dirs.append(os.path.dirname(path))
-
-            morfs = list(find_files(dirs, match='*.py', exclude=excl))
-
-            _coverobj.combine()
-            _coverobj.save()
-
-            if options.coverage:
-                _coverobj.report(morfs=morfs)
-            else:
-                dname = '_html'
-                _coverobj.html_report(morfs=morfs, directory=dname)
-                outfile = os.path.join(os.getcwd(), dname, 'index.html')
-
-                if sys.platform == 'darwin':
-                    os.system('open %s' % outfile)
-                else:
-                    webbrowser.get().open(outfile)
+        print(f"\nHTML report generated at: {index_file}")
 
-            shutil.copy(_coverobj.get_data().data_filename(),
-                        os.path.join(os.getcwd(), '.coverage'))
+    return cov
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/testflo/deprecations.py 
new/testflo-1.4.22/testflo/deprecations.py
--- old/testflo-1.4.15/testflo/deprecations.py  2023-10-23 21:57:00.000000000 
+0200
+++ new/testflo-1.4.22/testflo/deprecations.py  2020-02-02 01:00:00.000000000 
+0100
@@ -16,10 +16,11 @@
 
         deprecations = {}
 
-        for test in input_iter:
-            for msg, locs in test.deprecations.items():
-                deprecations[msg] = deprecations.get(msg, set()) | locs
-            yield test
+        for tests in input_iter:
+            for test in tests:
+                for msg, locs in test.deprecations.items():
+                    deprecations[msg] = deprecations.get(msg, set()) | locs
+                yield test
 
         report = self.generate_report(deprecations)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/testflo/discover.py 
new/testflo-1.4.22/testflo/discover.py
--- old/testflo-1.4.15/testflo/discover.py      2023-10-23 21:57:00.000000000 
+0200
+++ new/testflo-1.4.22/testflo/discover.py      2020-02-02 01:00:00.000000000 
+0100
@@ -61,7 +61,7 @@
         # process so that we can execute the module or class level 
setup/teardown
         # only once while impacting all of the tests in that group.
         new_tcase_groups = []
-        for tcase, tests in self._tcase_fixture_groups.items():
+        for tests in self._tcase_fixture_groups.values():
             tests = sorted(tests, key=lambda t: t.spec)
 
             # mark the first and last tests so that we know when to
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/testflo/filters.py 
new/testflo-1.4.22/testflo/filters.py
--- old/testflo-1.4.15/testflo/filters.py       2023-10-23 21:57:00.000000000 
+0200
+++ new/testflo-1.4.22/testflo/filters.py       2020-02-02 01:00:00.000000000 
+0100
@@ -1,5 +1,8 @@
 from __future__ import print_function
 
+import os
+
+
 class TimeFilter(object):
     """This iterator saves to the specified output file only those tests
     that complete successfully in max_time seconds or less.  This output
@@ -29,13 +32,17 @@
         self.outfile = outfile
 
     def get_iter(self, input_iter):
-        fails = []
+        try:
+            os.remove(self.outfile)
+        except OSError:
+            pass
+
         for result in input_iter:
             if result.status == 'FAIL' and not result.expected_fail:
-                fails.append(result.spec)
-            yield result
-
-        if fails:
-            with open(self.outfile, 'w') as f:
-                for spec in sorted(fails):
+                with open(self.outfile, 'a') as f:
+                    if result.nprocs > 1:
+                        spec = f"{result.spec}  # mpi, nprocs={result.nprocs}"
+                    else:
+                        spec = result.spec
                     print(spec, file=f)
+            yield result
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/testflo/isolatedrun.py 
new/testflo-1.4.22/testflo/isolatedrun.py
--- old/testflo-1.4.15/testflo/isolatedrun.py   2023-10-26 20:14:41.000000000 
+0200
+++ new/testflo-1.4.22/testflo/isolatedrun.py   2020-02-02 01:00:00.000000000 
+0100
@@ -5,12 +5,6 @@
 """
 
 if __name__ == '__main__':
-    try:
-        import coverage
-    except ImportError:
-        coverage = None
-    else:
-        coverage.process_startup()
 
     import sys
     import os
@@ -19,27 +13,31 @@
     from testflo.test import Test
     from testflo.qman import get_client_queue
     from testflo.options import get_options
+    from testflo.cover import setup_coverage
 
     queue = get_client_queue()
     os.environ['TESTFLO_QUEUE'] = ''
 
     options = get_options()
+    test = None
 
-    try:
-        try:
-            test = Test(sys.argv[1], options)
-            test.nocapture = True # so we don't lose stdout
-            test.run()
-        except:
-            print(traceback.format_exc())
-            test.status = 'FAIL'
-            test.err_msg = traceback.format_exc()
+    if options.coverage or options.coveragehtml:
+        cov = setup_coverage(options)
+    else:
+        cov = None
 
+    try:
+        test = Test(sys.argv[1], options)
+        test.nocapture = True # so we don't lose stdout
+        test.run(cov=cov)
     except:
-        test.err_msg = traceback.format_exc()
         test.status = 'FAIL'
+        test.err_msg = traceback.format_exc()
+    finally:
+        sys.stdout.flush()
+        sys.stderr.flush()
 
-    sys.stdout.flush()
-    sys.stderr.flush()
+        queue.put(test)
 
-    queue.put(test)
+        if cov is not None:
+            cov.save()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/testflo/main.py 
new/testflo-1.4.22/testflo/main.py
--- old/testflo-1.4.15/testflo/main.py  2023-11-13 17:01:09.000000000 +0100
+++ new/testflo-1.4.22/testflo/main.py  2020-02-02 01:00:00.000000000 +0100
@@ -29,8 +29,6 @@
 import time
 import warnings
 import multiprocessing
-import atexit
-import shutil
 
 from fnmatch import fnmatch, fnmatchcase
 
@@ -43,12 +41,15 @@
 from testflo.duration import DurationSummary
 from testflo.discover import TestDiscoverer
 from testflo.filters import TimeFilter, FailFilter
+from testflo.cover import setup_coverage, finalize_coverage
 
 from testflo.util import read_config_file, read_test_file
 from testflo.options import get_options
 from testflo.qman import get_server_queue
-
-options = get_options()
+try:
+    import coverage
+except ImportError:
+    coverage = None
 
 
 def dryrun(input_iter):
@@ -59,7 +60,11 @@
         for test in tests:
             if test.status is None:
                 test.status = 'OK'
-            print(test.spec)
+            if test.nprocs > 1:
+                spec = f"{test.spec}  # mpi, nprocs={test.nprocs}"
+            else:
+                spec = test.spec
+            print(spec)
             yield test
 
 
@@ -120,22 +125,12 @@
 
     nprocs = options.num_procs
 
-    options.skip_dirs = []
-
     # read user prefs from ~/.testflo file.
     # create one if it doesn't exist
     homedir = os.path.expanduser('~')
     rcfile = os.path.join(homedir, '.testflo')
-    if not os.path.isfile(rcfile):
-        with open(rcfile, 'w') as f:
-            f.write("""[testflo]
-skip_dirs=site-packages,
-    dist-packages,
-    build,
-    _build,
-    contrib
-""")
-    read_config_file(rcfile, options)
+    if os.path.isfile(rcfile):
+        read_config_file(rcfile, options)
     if options.cfg:
         read_config_file(options.cfg, options)
 
@@ -156,8 +151,14 @@
     if not tests:
         tests = [os.getcwd()]
 
+    always_skip = {'site-packages', 'dist-packages', '__pycache__', 'build', 
'_build',
+                   '.pixi'}
+
     def dir_exclude(d):
         base = os.path.basename(d)
+        if base in always_skip:
+            return True
+
         for skip in options.skip_dirs:
             if fnmatch(base, skip):
                 return True
@@ -167,21 +168,21 @@
     os.environ['TESTFLO_RUNNING'] = '1'
 
     if options.coverage or options.coveragehtml:
-        os.environ['TESTFLO_MAIN_PID'] = str(os.getpid())
-        # some coverage files aren't written until atexit of their processes, 
so put our finalize
-        # routine in atexit before their Coverage._atexit methods are 
registered so ours will be
-        # executed *after* theirs.
-        def _finalize():
-            if os.getpid() == int(os.environ.get('TESTFLO_MAIN_PID', '0')):
-                from testflo.cover import finalize_coverage
-                finalize_coverage(options)
-                # clean up the temporary dir where we store the interim 
coverage files from all
-                # of the processes.
-                if os.path.isdir('_covdir'):
-                    shutil.rmtree('_covdir')
-        atexit.register(_finalize)
-        from testflo.cover import setup_coverage
-        setup_coverage(options)
+        cov_dir = options.cover_dir or os.getcwd()
+        options.cover_dir = os.path.abspath(cov_dir)
+        if options.cover_omits is None:
+            options.cover_omits = []
+        options.cover_omits.append('*/testflo/*')
+
+        if not coverage:
+            raise RuntimeError("coverage has not been installed.")
+        if not options.coverpkgs:
+            raise RuntimeError("No packages specified for coverage. "
+                               "Use the --coverpkg option to add a package.")
+
+        cov = setup_coverage(options)
+    else:
+        cov = None
 
     if options.noreport:
         report_file = open(os.devnull, 'a')
@@ -232,7 +233,7 @@
             if options.pre_announce:
                 options.num_procs = 1
 
-            pipeline.append(ConcurrentTestRunner(options, queue).get_iter)
+            pipeline.append(ConcurrentTestRunner(options, queue, 
cov=cov).get_iter)
 
             if options.show_deprecations or options.deprecations_report:
                 pipeline.append(DeprecationsReport(options).get_iter)
@@ -271,6 +272,8 @@
         if manager is not None:
             manager.shutdown()
 
+    finalize_coverage(options, cov)
+
     return retval
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/testflo/mpirun.py 
new/testflo-1.4.22/testflo/mpirun.py
--- old/testflo-1.4.15/testflo/mpirun.py        2023-10-26 20:14:41.000000000 
+0200
+++ new/testflo-1.4.22/testflo/mpirun.py        2020-02-02 01:00:00.000000000 
+0100
@@ -6,12 +6,6 @@
 """
 
 if __name__ == '__main__':
-    try:
-        import coverage
-    except ImportError:
-        pass
-    else:
-        coverage.process_startup()
 
     import sys
     import os
@@ -24,6 +18,7 @@
     from testflo.test import Test
     from testflo.qman import get_client_queue
     from testflo.options import get_options
+    from testflo.cover import setup_coverage
 
     exitcode = 0  # use 0 for exit code of all ranks != 0 because otherwise,
                   # MPI will terminate other processes
@@ -32,13 +27,19 @@
     os.environ['TESTFLO_QUEUE'] = ''
 
     options = get_options()
+    test = None
+
+    if options.coverage or options.coveragehtml:
+        cov = setup_coverage(options)
+    else:
+        cov = None
 
     try:
         try:
             comm = MPI.COMM_WORLD
             test = Test(sys.argv[1], options)
             test.nocapture = True # so we don't lose stdout
-            test.run()
+            test.run(cov=cov)
         except:
             print(traceback.format_exc())
             test.status = 'FAIL'
@@ -71,3 +72,6 @@
 
         if comm.rank == 0:
             queue.put(test)
+
+        if cov is not None:
+            cov.save()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/testflo/printer.py 
new/testflo-1.4.22/testflo/printer.py
--- old/testflo-1.4.15/testflo/printer.py       2023-10-23 21:57:00.000000000 
+0200
+++ new/testflo-1.4.22/testflo/printer.py       2020-02-02 01:00:00.000000000 
+0100
@@ -43,7 +43,7 @@
 
         if (self.verbose == 0 and (result.err_msg and show_msg)) or 
self.verbose > 0:
             if result.mpi and result.nprocs > 0:
-                run_type = '(mpi) '
+                run_type = f'(mpi {result.nprocs}) '
             elif result.isolated:
                 run_type = '(isolated) '
             else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/testflo/runner.py 
new/testflo-1.4.22/testflo/runner.py
--- old/testflo-1.4.15/testflo/runner.py        2021-12-12 23:38:56.000000000 
+0100
+++ new/testflo-1.4.22/testflo/runner.py        2020-02-02 01:00:00.000000000 
+0100
@@ -8,43 +8,45 @@
 
 from multiprocessing import Queue, Process
 
-from testflo.cover import save_coverage
-from testflo.test import Test
-from testflo.options import get_options
+from testflo.cover import setup_coverage
 
 
-def worker(test_queue, done_queue, subproc_queue, worker_id):
-    """This is used by concurrent test processes. It takes a test
+def worker(test_queue, done_queue, subproc_queue, worker_id, options):
+    """This is used by concurrent test processes. It takes a Test object
     off of the test_queue, runs it, then puts the Test object
     on the done_queue.
     """
+
+    cov = setup_coverage(options)
+
     test_count = 0
-    for tests in iter(test_queue.get, 'STOP'):
+    try:
+        for tests in iter(test_queue.get, 'STOP'):
 
-        done_tests = []
-        for test in tests:
-            try:
-                test_count += 1
-                done_tests.append(test.run(subproc_queue))
-            except:
-                # we generally shouldn't get here, but just in case,
-                # handle it so that the main process doesn't hang at the
-                # end when it tries to join all of the concurrent processes.
-                done_tests.append(test)
-
-        done_queue.put(done_tests)
-
-    # don't save anything unless we actually ran a test
-    if test_count > 0:
-        save_coverage()
+            done_tests = []
+            for test in tests:
+                try:
+                    test_count += 1
+                    done_tests.append(test.run(subproc_queue, cov=cov))
+                except:
+                    # we generally shouldn't get here, but just in case,
+                    # handle it so that the main process doesn't hang at the
+                    # end when it tries to join all of the concurrent 
processes.
+                    done_tests.append(test)
+
+            done_queue.put(done_tests)
+    finally:
+        if cov:
+            cov.save()
 
 
 class TestRunner(object):
 
-    def __init__(self, options, subproc_queue):
+    def __init__(self, options, subproc_queue, cov):
         self.stop = options.stop
         self.pre_announce = options.pre_announce
         self._queue = subproc_queue
+        self.cov = cov
 
     def get_iter(self, input_iter):
         """Run tests serially."""
@@ -55,7 +57,7 @@
                 if self.pre_announce:
                     print("    about to run %s " % test.short_name(), end='')
                     sys.stdout.flush()
-                result = test.run(self._queue)
+                result = test.run(self._queue, cov=self.cov)
                 yield result
                 if self.stop:
                     if (result.status == 'FAIL' and not result.expected_fail) 
or (
@@ -65,16 +67,14 @@
             if stop:
                 break
 
-        save_coverage()
-
 
 class ConcurrentTestRunner(TestRunner):
     """TestRunner that uses the multiprocessing package
     to execute tests concurrently.
     """
 
-    def __init__(self, options, subproc_queue):
-        super(ConcurrentTestRunner, self).__init__(options, subproc_queue)
+    def __init__(self, options, subproc_queue, cov):
+        super(ConcurrentTestRunner, self).__init__(options, subproc_queue, cov)
         self.num_procs = options.num_procs
 
         # only do concurrent stuff if num_procs > 1
@@ -84,7 +84,6 @@
             # Create queues
             self.task_queue = Queue()
             self.done_queue = Queue()
-
             self.procs = []
 
             # Start worker processes
@@ -93,7 +92,7 @@
                 self.procs.append(Process(target=worker,
                                           args=(self.task_queue,
                                                 self.done_queue, subproc_queue,
-                                                worker_id)))
+                                                worker_id, options)))
 
             for proc in self.procs:
                 proc.start()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/testflo/test.py 
new/testflo-1.4.22/testflo/test.py
--- old/testflo-1.4.15/testflo/test.py  2023-10-26 20:14:41.000000000 +0200
+++ new/testflo-1.4.22/testflo/test.py  2020-02-02 01:00:00.000000000 +0100
@@ -2,6 +2,7 @@
 
 import os
 import sys
+import shutil
 import time
 import warnings
 import traceback
@@ -15,19 +16,16 @@
 from unittest import TestCase, SkipTest
 from unittest.case import _UnexpectedSuccess
 
-from testflo.cover import start_coverage, stop_coverage
-
 from testflo.util import get_module, ismethod, get_memory_usage, \
-                         get_testpath, _options2args
+                         get_testpath, _options2args, _testing_path
 from testflo.utresult import UnitTestResult
 from testflo.devnull import DevNull
 
 
-from distutils import spawn
 mpirun_exe = None
-if spawn.find_executable("mpirun") is not None:
+if shutil.which("mpirun") is not None:
     mpirun_exe = "mpirun"
-elif spawn.find_executable("mpiexec") is not None:
+elif shutil.which("mpiexec") is not None:
     mpirun_exe = "mpiexec"
 
 
@@ -46,17 +44,18 @@
         self.size = 1
 
 
-# create a copy of sys.path with an extra entry at the beginning so that
-# we can quickly replace the first entry with the curent test's dir rather
-# than constantly copying the whole sys.path
-_testing_path = ['.'] + sys.path
-
-
 @contextmanager
-def testcontext(test):
+def testcontext(test, cov):
+    if cov is not None:
+        cov.start()
+        if test.options.dyn_contexts:
+            cov.switch_context(test.spec)
+
     global _testing_path
     old_sys_path = sys.path
 
+    os.environ['TESTFLO_SPEC'] = test.spec
+
     _testing_path[0] = test.test_dir
     sys.path = _testing_path
 
@@ -66,7 +65,11 @@
         test.status = 'FAIL'
         test.err_msg = traceback.format_exc()
     finally:
+        if cov is not None:
+            cov.stop()
         sys.path = old_sys_path
+        if 'TESTFLO_SPEC' in os.environ:
+            del os.environ['TESTFLO_SPEC']
 
 
 class Test(object):
@@ -115,7 +118,7 @@
         """Get the test's module, testcase (if any), function name,
         N_PROCS (for mpi tests) and ISOLATED and set our attributes.
         """
-        with testcontext(self):
+        with testcontext(self, None):
             try:
                 mod, self.tcasename, self.funcname = 
_parse_test_path(self.spec)
                 self.modpath = mod.__name__
@@ -205,7 +208,7 @@
             cmd =  [mpirun_exe, '-n', str(self.nprocs),
                    sys.executable,
                    os.path.join(os.path.dirname(__file__), 'mpirun.py'),
-                   self.spec] + _options2args()
+                   self.spec] + _options2args(self.options)
 
             result = self._run_subproc(cmd, queue, os.environ)
 
@@ -222,7 +225,7 @@
 
         return result
 
-    def run(self, queue=None):
+    def run(self, queue=None, cov=None):
         """Runs the test, assuming status is not already known."""
         if self.status is not None:
             # premature failure occurred (or dry run), just return
@@ -240,7 +243,7 @@
         elif self.options.isolated:
             return self._run_isolated(queue)
 
-        with testcontext(self):
+        with testcontext(self, cov):
             testpath, _ = get_testpath(self.spec)
             _, mod = get_module(testpath)
 
@@ -287,8 +290,6 @@
                 sys.stdout = outstream
                 sys.stderr = errstream
 
-                start_coverage()
-
                 self.start_time = time.perf_counter()
 
                 catch_deps = self.options.show_deprecations or 
self.options.deprecations_report
@@ -341,11 +342,13 @@
                         else:
                             stream_val = ''
                         if ut_subtests:
+                            end_time = time.perf_counter()
                             for sub, err in ut_subtests:
                                 subtest = SubTest(sub._subDescription(), 
self.spec, self.options)
                                 subtest.status = status
                                 subtest.err_msg = stream_val + err
-                                subtest.end_time = time.perf_counter()
+                                subtest.start_time = self.start_time
+                                subtest.end_time = end_time
                                 subtest.memory_usage = get_memory_usage()
                                 subs.append(subtest)
                         else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/testflo/tests/test_nested_fixtures.py 
new/testflo-1.4.22/testflo/tests/test_nested_fixtures.py
--- old/testflo-1.4.15/testflo/tests/test_nested_fixtures.py    1970-01-01 
01:00:00.000000000 +0100
+++ new/testflo-1.4.22/testflo/tests/test_nested_fixtures.py    2020-02-02 
01:00:00.000000000 +0100
@@ -0,0 +1,67 @@
+
+import os
+
+import unittest
+
+modpid = None
+
+def setUpModule():
+    global modpid
+    modpid = os.getpid()
+    print("\ncalled setUpModule from pid %d\n" % modpid)
+
+def tearDownModule():
+    global modpid
+    mypid = os.getpid()
+    assert mypid == modpid
+    print("\ncalled tearDownModule from pid %d\n" % mypid)
+
+
+class TestfloTestCaseWFixture2(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        cls.pid = os.getpid()
+        print("\nsetting up %s, pid=%d\n" % (cls.__name__, cls.pid))
+
+    @classmethod
+    def tearDownClass(cls):
+        assert os.getpid() == cls.pid
+        print("\ntearing down %s, pid=%d\n" % (cls.__name__, cls.pid))
+
+    def test_tcase_grouped_ok(self):
+        assert os.getpid() == self.pid
+
+    def test_tcase_grouped_fail(self):
+        self.fail("failure 3")
+
+    @unittest.expectedFailure
+    def test_tcase_grouped_expected_fail(self):
+        self.fail("I expected this")
+
+    @unittest.expectedFailure
+    def test_tcase_grouped_unexpected_success(self):
+        pass
+
+    @unittest.skip("skipping 2")
+    def test_tcase_grouped_skip(self):
+        self.fail("This test should have been skipped.")
+
+
[email protected]("skipping a whole testcase...")
+class SkippedTestCase2(unittest.TestCase):
+    def test_1(self):
+        self.fail("This test should have been skipped.")
+
+    def test_2(self):
+        self.fail("This test should have been skipped.")
+
+    def test_3(self):
+        self.fail("This test should have been skipped.")
+
+    def test_4(self):
+        self.fail("This test should have been skipped.")
+
+
+if __name__ == '__main__':
+    unittest.main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/testflo/tests/test_testflo.py 
new/testflo-1.4.22/testflo/tests/test_testflo.py
--- old/testflo-1.4.15/testflo/tests/test_testflo.py    1970-01-01 
01:00:00.000000000 +0100
+++ new/testflo-1.4.22/testflo/tests/test_testflo.py    2020-02-02 
01:00:00.000000000 +0100
@@ -0,0 +1,104 @@
+
+import os
+
+import unittest
+
+class TestfloTestCase(unittest.TestCase):
+    def test_ok(self):
+        pass
+
+    def test_env_var(self):
+        testflo_running = os.getenv("TESTFLO_RUNNING", default=False)
+        self.assertNotEqual(testflo_running, False)
+
+    def test_fail(self):
+        self.fail("This test should fail")
+
+    @unittest.expectedFailure
+    def test_expected_fail_good(self):
+        self.fail("I expected this")
+
+    @unittest.expectedFailure
+    def test_unexpected_success(self):
+        pass
+
+    @unittest.skip("skipping 1")
+    def test_skip(self):
+        self.fail("This test should have been skipped.")
+
+class TestfloTestCaseWFixture(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        cls.pid = os.getpid()
+        print("setting up %s, pid=%d" % (cls.__name__, cls.pid))
+
+    @classmethod
+    def tearDownClass(cls):
+        assert os.getpid() == cls.pid
+        print("tearing down %s, pid=%d" % (cls.__name__, cls.pid))
+
+    def test_tcase_grouped_ok(self):
+        assert os.getpid() == self.pid
+
+    def test_tcase_grouped_fail(self):
+        self.fail("failure 2")
+
+    @unittest.expectedFailure
+    def test_tcase_grouped_expected_fail(self):
+        self.fail("I expected this")
+
+    @unittest.expectedFailure
+    def test_tcase_grouped_unexpected_success(self):
+        pass
+
+    @unittest.skip("skipping 2")
+    def test_tcase_grouped_skip(self):
+        pass
+
+
[email protected]("skipping a whole testcase...")
+class SkippedTestCase(unittest.TestCase):
+    def test_1(self):
+        self.fail("This test should have been skipped.")
+
+    def test_2(self):
+        self.fail("This test should have been skipped.")
+
+    def test_3(self):
+        self.fail("This test should have been skipped.")
+
+    def test_4(self):
+        self.fail("This test should have been skipped.")
+
+
+class TestSubTests(unittest.TestCase):
+    def test_subtests(self):
+        # Base price by item
+        base_price = {"gum": 1.00, "milk": 2.50, "eggs": 2.75}
+        # Sales tax by state
+        sales_tax = {"Michigan": 0.06, "Ohio": 0.0575, "New Hampshire": 0.00}
+
+        # Loop through each state and item and precompute expected price
+        precalculated_price = {}
+        for state in sales_tax:
+            precalculated_price[state] = {}
+            for item in base_price:
+                precalculated_price[state][item] = (1.0 + sales_tax[state]) * 
base_price[item]
+
+        # Intentionally mess up michigan price for gum and ohio price for eggs
+        precalculated_price["Michigan"]["gum"] = 100.0
+        precalculated_price["Ohio"]["eggs"] = -3.14159
+
+        # Run through nested subtests, by state and item, and double check 
that logged price matches expected
+        for state in sales_tax:
+            with self.subTest(state=state):
+                for item in base_price:
+                    with self.subTest(item=item):
+                        expected_price = (1.0 + sales_tax[state]) * 
base_price[item]
+                        logged_price = precalculated_price[state][item]
+                        assert logged_price == expected_price
+
+
+class TestSubTestsMPI(TestSubTests):
+    N_PROCS = 2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/testflo/util.py 
new/testflo-1.4.22/testflo/util.py
--- old/testflo-1.4.15/testflo/util.py  2023-10-26 20:14:41.000000000 +0200
+++ new/testflo-1.4.22/testflo/util.py  2020-02-02 01:00:00.000000000 +0100
@@ -17,7 +17,12 @@
 
 from argparse import ArgumentParser, _AppendAction
 
-from testflo.cover import start_coverage, stop_coverage
+
+# create a copy of sys.path with an extra entry at the beginning so that
+# we can quickly replace the first entry with the curent test's dir rather
+# than constantly copying the whole sys.path
+_testing_path = ['.'] + sys.path
+
 
 _store = {}
 
@@ -89,7 +94,13 @@
     parser.add_argument('--cover-omit', action='append', dest='cover_omits',
                         metavar='FILE',
                         help="Add a file name pattern to remove it from 
coverage.")
-
+    parser.add_argument('--cover-branch', action='store_true', 
dest='cover_branch',
+                        help="Enable branch coverage (more detailed but 
slower). Default is False.")
+    parser.add_argument('--cover-dir', action='store', dest='cover_dir',
+                        metavar='DIR', help="Specify the coverage dir to use. 
Default is the "
+                        "current working directory.")
+    parser.add_argument('--cover-contexts', action='store_true', 
dest='dyn_contexts',
+                        help="Record which tests hit which lines (increases 
overhead)."),
     parser.add_argument('-b', '--benchmark', action='store_true', 
dest='benchmark',
                         help='Specifies that benchmarks are to be run rather '
                              'than tests, so only files starting with 
"benchmark_" '
@@ -147,31 +158,46 @@
     return parser
 
 
-def _options2args():
-    """Gets the testflo args that should be used in subprocesses."""
+def _options2args(options):
+    """Gets the testflo args that should be passed to subprocesses."""
+
+    store_args = set([
+      'coverage_dir',
+    ])
 
-    cmdset = set([
-      '--nocapture',
-      '-s',
-      '--coverpkg',
-      '--coverage',
-      '--coverage-html',
-      '--cover-omit',
+    multi_args = set([
+      'coverpkg',
+      'cover_omits',
     ])
 
+    store_true_args = set([
+      'coverage',
+      'coveragehtml',
+      'nocapture',
+      'cover_branch',
+      'dyn_contexts',
+    ])
+
+    all_args = store_args | store_true_args | multi_args
+
+    parser = _get_parser()
+    dest_to_flags = {
+        action.dest: (action.option_strings, action.default) for action in 
parser._actions
+            if action.dest in all_args
+        }
+
     keep = []
-    i = 0
-    args = sys.argv[1:]
-    argslen = len(args)
-    while i < argslen:
-        arg = args[i]
-        if arg.split('=',1)[0] in cmdset:
-            keep.append(arg)
-            if ((arg.startswith('--coverpkg') or 
arg.startswith('--cover-omit'))
-                        and '=' not in arg):
-                i += 1
-                keep.append(args[i])
-        i += 1
+    for dest, (flags, default) in dest_to_flags.items():
+        opt = getattr(options, dest)
+        if dest in store_args:
+            if opt != default:
+                keep.append(f"{flags[0]}={opt}")
+        elif dest in multi_args:
+            if opt:
+                for oparg in opt:
+                    keep.append(f"{flags[0]}={oparg}")
+        elif dest in store_true_args and opt:
+            keep.append(flags[0])
 
     return keep
 
@@ -337,7 +363,11 @@
 
 
 def try_import(fname, modpath):
+    global _testing_path
     try:
+        _testing_path[0] = os.path.dirname(fname)
+        old_sys_path = sys.path
+        sys.path = _testing_path
         mod = import_module(modpath)
     except ImportError:
         # this might be a module that's not in the same
@@ -356,6 +386,8 @@
             del sys.modules[modpath]
         finally:
             sys.path = oldpath
+    finally:
+        sys.path = old_sys_path
 
     return mod
 
@@ -446,7 +478,9 @@
                 typ = lambda x: x
 
             if isinstance(action, _AppendAction):
-                setattr(options, name, [typ(s.strip()) for s in 
optstr.split(',') if s.strip()])
+                oldval =  getattr(options, name)
+                addval = [typ(s.strip()) for s in optstr.split(',') if 
s.strip()]
+                setattr(options, name, oldval+addval)
             else:
                 setattr(options, name, typ(optstr))
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/testflo.egg-info/PKG-INFO 
new/testflo-1.4.22/testflo.egg-info/PKG-INFO
--- old/testflo-1.4.15/testflo.egg-info/PKG-INFO        2023-12-28 
19:25:25.000000000 +0100
+++ new/testflo-1.4.22/testflo.egg-info/PKG-INFO        1970-01-01 
01:00:00.000000000 +0100
@@ -1,202 +0,0 @@
-Metadata-Version: 2.1
-Name: testflo
-Version: 1.4.15
-Summary: A simple flow-based testing framework
-License: Apache 2.0
-Classifier: Development Status :: 4 - Beta
-Classifier: License :: OSI Approved :: Apache Software License
-Classifier: Natural Language :: English
-Classifier: Operating System :: MacOS :: MacOS X
-Classifier: Operating System :: POSIX :: Linux
-Classifier: Operating System :: Microsoft :: Windows
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: Implementation :: CPython
-Description-Content-Type: text/markdown
-License-File: LICENSE.txt
-Requires-Dist: coverage>=6.0
-
-[![PyPI version][1]][2]
-
-testflo
-=======
-
-testflo is a python testing framework that uses a pipeline of
-iterators to process test specifications, run the tests, and process the
-results.
-
-Why write another testing framework?
-------------------------------------
-
-testflo was written to support testing of the OpenMDAO framework.
-Some OpenMDAO features require execution under MPI while some others don't,
-so we wanted a testing framework that could run all of our tests in the same
-way and would allow us to build all of our tests using unittest.TestCase
-objects that we were already familiar with.  The MPI testing functionality
-was originally implemented using the nose testing framework.  It worked, but
-was always buggy, and the size and complexity of the nose framework made it
-difficult to know exactly what was going on.
-
-Enter testflo, an attempt to build a simpler testing framework that would have
-the basic functionality of other test frameworks, with the additional
-ability to run MPI unit tests that are very similar to regular unit tests.
-
-
-Some testflo features
----------------------
-
-*    MPI unit testing
-*    *pre_announce* option to print test name before running in order to
-     quickly identify hanging MPI tests
-*    concurrent testing  (on by default, use '-n 1' to turn it off)
-*    test coverage
-*    flexible execution - can be given a directory, a file, a module path,
-     *file:testcase.method*, *module:testcase.method*, or a file containing
-     a list of any of the above. Has options to generate test list files
-     containing all failed tests or all tests that execute within a certain
-     time limit.
-*    end of testing summary
-
-
-Usage
------
-
-For a full list of testflo options, execute the following:
-
-`testflo -h`
-
-
-NOTE: Because testflo runs tests concurrently by default, your tests must be
-written with concurrency in mind or they may fail.  For example, if multiple
-tests write output to a file with the same name, you have to make sure that 
those
-tests are executed in different directories to prevent that file from being
-corrupted.  If your tests are not written to run concurrently, you can always
-just run them with `testflo -n 1` and run them in serial instead.
-
-The following is an example of what an MPI unit test looks like.  To tell
-testflo that a TestCase is an MPI TestCase, you add a class attribute
-called N_PROCS to it and set it to the number of MPI processes to use for the
-test.  That's all there is to it. Of course, depending on what sort of MPI code
-you're testing, it's up to you to potentially test for different things on
-different ranks.
-
-
-```python
-
-class MyMPI_TestCase(TestCase):
-
-    N_PROCS = 4  # this is how many MPI processes to use for this TestCase.
-
-    def test_foo(self):
-
-        # do your MPI testing here, e.g.,
-
-        if self.comm.rank == 0:
-            # some test only valid on rank 0...
-
-
-```
-
-
-Here's an example of testflo output for openmdao.core:
-
-
-```
-
-openmdao$ testflo openmdao.core
-............................................................................
-............................................................................
-............................................................................
-..............................
-
-OK
-
-Passed:  258
-Failed:  0
-Skipped: 0
-
-
-Ran 258 tests using 8 processes
-Wall clock time:   00:00:1.82
-
-```
-
-Running testflo in verbose mode on openmdao.core.test.test_problem is shown
-below. The verbose output contains the full test name as well as the elapsed
-time and memory usage.
-
-
-```
-
-openmdao$ testflo openmdao.core.test.test_problem -v
-openmdao.core.test.test_problem:TestCheckSetup.test_pbo_messages ... OK 
(00:00:0.02, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_check_promotes ... OK 
(00:00:0.01, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_conflicting_connections ... 
OK (00:00:0.02, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_conflicting_promoted_state_vars
 ... OK (00:00:0.00, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_conflicting_promotions ... OK 
(00:00:0.01, 69 MB)
-openmdao.core.test.test_problem:TestCheckSetup.test_out_of_order ... OK 
(00:00:0.02, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_explicit_connection_errors 
... OK (00:00:0.02, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_find_subsystem ... OK 
(00:00:0.00, 69 MB)
-openmdao.core.test.test_problem:TestCheckSetup.test_cycle ... OK (00:00:0.06, 
69 MB)
-openmdao.core.test.test_problem:TestProblem.test_input_input_explicit_conns_no_conn
 ... OK (00:00:0.01, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_illegal_desvar ... OK 
(00:00:0.01, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_input_input_explicit_conns_w_conn
 ... OK (00:00:0.02, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_check_connections ... OK 
(00:00:0.06, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_mode_auto ... OK (00:00:0.03, 
69 MB)
-openmdao.core.test.test_problem:TestProblem.test_check_parallel_derivs ... OK 
(00:00:0.01, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_simplest_run ... OK 
(00:00:0.01, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_basic_run ... OK (00:00:0.03, 
69 MB)
-openmdao.core.test.test_problem:TestProblem.test_change_solver_after_setup ... 
OK (00:00:0.04, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_no_vecs ... OK (00:00:0.08, 
69 MB)
-openmdao.core.test.test_problem:TestProblem.test_src_idx_gt_src_size ... OK 
(00:00:0.01, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_src_idx_neg ... OK 
(00:00:0.01, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_simplest_run_w_promote ... OK 
(00:00:0.02, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_unconnected_param_access ... 
OK (00:00:0.01, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_variable_access_before_setup 
... OK (00:00:0.00, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_scalar_sizes ... OK 
(00:00:0.07, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_byobj_run ... OK (00:00:0.01, 
69 MB)
-openmdao.core.test.test_problem:TestProblem.test_error_change_after_setup ... 
OK (00:00:0.31, 70 MB)
-openmdao.core.test.test_problem:TestProblem.test_unconnected_param_access_with_promotes
 ... OK (00:00:0.04, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_variable_access ... OK 
(00:00:0.06, 69 MB)
-openmdao.core.test.test_problem:TestProblem.test_iprint ... OK (00:00:0.25, 73 
MB)
-
-
-OK
-
-Passed:  30
-Failed:  0
-Skipped: 0
-
-
-Ran 30 tests using 8 processes
-Wall clock time:   00:00:1.17
-
-```
-
-Operating Systems and Python Versions
--------------------------------------
-
-testflo is used to test OpenMDAO as part of its CI process,
-so we run it nearly every day on linux, Windows and OS X. It requires
-python 3.5 or higher.
-
-
-You can install testflo directly from github using the following command:
-
-`pip install git+https://github.com/OpenMDAO/testflo.git`
-
-
-or install from PYPI using:
-
-
-`pip install testflo`
-
-
-
-If you try it out and find any problems, submit them as issues on github at
-https://github.com/OpenMDAO/testflo.
-
-[1]: https://badge.fury.io/py/testflo.svg "PyPI Version"
-[2]: https://badge.fury.io/py/testflo "testflo @PyPI"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/testflo.egg-info/SOURCES.txt 
new/testflo-1.4.22/testflo.egg-info/SOURCES.txt
--- old/testflo-1.4.15/testflo.egg-info/SOURCES.txt     2023-12-28 
19:25:25.000000000 +0100
+++ new/testflo-1.4.22/testflo.egg-info/SOURCES.txt     1970-01-01 
01:00:00.000000000 +0100
@@ -1,32 +0,0 @@
-DESIGN.txt
-LICENSE.txt
-MANIFEST.in
-README.md
-setup.cfg
-setup.py
-testflo/__init__.py
-testflo/__main__.py
-testflo/benchmark.py
-testflo/cover.py
-testflo/deprecations.py
-testflo/devnull.py
-testflo/discover.py
-testflo/duration.py
-testflo/filters.py
-testflo/isolatedrun.py
-testflo/main.py
-testflo/mpirun.py
-testflo/options.py
-testflo/printer.py
-testflo/qman.py
-testflo/runner.py
-testflo/summary.py
-testflo/test.py
-testflo/util.py
-testflo/utresult.py
-testflo.egg-info/PKG-INFO
-testflo.egg-info/SOURCES.txt
-testflo.egg-info/dependency_links.txt
-testflo.egg-info/entry_points.txt
-testflo.egg-info/requires.txt
-testflo.egg-info/top_level.txt
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/testflo.egg-info/dependency_links.txt 
new/testflo-1.4.22/testflo.egg-info/dependency_links.txt
--- old/testflo-1.4.15/testflo.egg-info/dependency_links.txt    2023-12-28 
19:25:25.000000000 +0100
+++ new/testflo-1.4.22/testflo.egg-info/dependency_links.txt    1970-01-01 
01:00:00.000000000 +0100
@@ -1 +0,0 @@
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/testflo.egg-info/entry_points.txt 
new/testflo-1.4.22/testflo.egg-info/entry_points.txt
--- old/testflo-1.4.15/testflo.egg-info/entry_points.txt        2023-12-28 
19:25:25.000000000 +0100
+++ new/testflo-1.4.22/testflo.egg-info/entry_points.txt        1970-01-01 
01:00:00.000000000 +0100
@@ -1,2 +0,0 @@
-[console_scripts]
-testflo = testflo.main:main
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/testflo.egg-info/requires.txt 
new/testflo-1.4.22/testflo.egg-info/requires.txt
--- old/testflo-1.4.15/testflo.egg-info/requires.txt    2023-12-28 
19:25:25.000000000 +0100
+++ new/testflo-1.4.22/testflo.egg-info/requires.txt    1970-01-01 
01:00:00.000000000 +0100
@@ -1 +0,0 @@
-coverage>=6.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/testflo-1.4.15/testflo.egg-info/top_level.txt 
new/testflo-1.4.22/testflo.egg-info/top_level.txt
--- old/testflo-1.4.15/testflo.egg-info/top_level.txt   2023-12-28 
19:25:25.000000000 +0100
+++ new/testflo-1.4.22/testflo.egg-info/top_level.txt   1970-01-01 
01:00:00.000000000 +0100
@@ -1 +0,0 @@
-testflo

Reply via email to