[med-svn] [Git][med-team/cwltest][upstream] New upstream version 2.6.20250818005349

Michael R. Crusoe (@crusoe) gitlab at salsa.debian.org
Tue Sep 30 14:49:37 BST 2025



Michael R. Crusoe pushed to branch upstream at Debian Med / cwltest


Commits:
68eb88a7 by Michael R. Crusoe at 2025-09-30T14:56:01+02:00
New upstream version 2.6.20250818005349
- - - - -


29 changed files:

- .github/workflows/ci-tests.yml
- .github/workflows/codeql-analysis.yml
- Makefile
- PKG-INFO
- README.rst
- cwltest.egg-info/PKG-INFO
- cwltest.egg-info/SOURCES.txt
- cwltest.egg-info/requires.txt
- cwltest/_version.py
- cwltest/argparser.py
- cwltest/compare.py
- cwltest/hooks.py
- cwltest/main.py
- cwltest/plugin.py
- cwltest/utils.py
- dev-requirements.txt
- docs/requirements.txt
- mypy-requirements.txt
- pyproject.toml
- requirements.txt
- tests/test_badgedir.py
- tests/test_categories.py
- tests/test_compare.py
- + tests/test_numeric_id.py
- tests/test_plugin.py
- tests/test_short_names.py
- tests/test_timeout.py
- tests/util.py
- tox.ini


Changes:

=====================================
.github/workflows/ci-tests.yml
=====================================
@@ -15,14 +15,14 @@ jobs:
   tox:
     name: CI tests via Tox
 
-    runs-on: ubuntu-20.04  # 22.04 doesn't support Python 3.6
+    runs-on: ubuntu-latest
 
     strategy:
       fail-fast: false
       matrix:
         # The README.rst file mentions the versions tested, please update it as well
         py-ver-major: [3]
-        py-ver-minor: [8, 9, 10, 11, 12, 13]
+        py-ver-minor: [9, 10, 11, 12, 13, 14]
         step: [lint, unit, mypy, bandit]
 
     env:
@@ -30,7 +30,7 @@ jobs:
       TOXENV: ${{ format('py{0}{1}-{2}', matrix.py-ver-major, matrix.py-ver-minor, matrix.step) }}
 
     steps:
-      - uses: actions/checkout at v4
+      - uses: actions/checkout at v5
         with:
           fetch-depth: 0
 
@@ -50,6 +50,9 @@ jobs:
           pip install -U pip setuptools wheel
           pip install "tox>4,<5" "tox-gh-actions>3"
 
+      - name: install dev libraries
+        run: sudo apt-get install -y libxml2-dev libxslt-dev
+
       - name: MyPy cache
         if: ${{ matrix.step == 'mypy' }}
         uses: actions/cache at v4
@@ -70,18 +73,18 @@ jobs:
   tox-style:
     name: CI linters via Tox
 
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-24.04
 
     strategy:
       matrix:
         step: [lintreadme, pydocstyle]
 
     env:
-      py-semver: "3.12"
-      TOXENV: ${{ format('py312-{0}', matrix.step) }}
+      py-semver: "3.13"
+      TOXENV: ${{ format('py313-{0}', matrix.step) }}
 
     steps:
-      - uses: actions/checkout at v4
+      - uses: actions/checkout at v5
         with:
           fetch-depth: 0
 
@@ -110,15 +113,15 @@ jobs:
   release_test:
     name: cwltest release test
 
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-24.04
 
     steps:
-      - uses: actions/checkout at v4
+      - uses: actions/checkout at v5
 
       - name: Set up Python
         uses: actions/setup-python at v5
         with:
-          python-version: "3.12"
+          python-version: "3.13"
           cache: pip
           cache-dependency-path: |
             requirements.txt


=====================================
.github/workflows/codeql-analysis.yml
=====================================
@@ -19,7 +19,7 @@ jobs:
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout at v4
+      uses: actions/checkout at v5
       with:
         # We must fetch at least the immediate parents so that if this is
         # a pull request then we can checkout the head.


=====================================
Makefile
=====================================
@@ -29,7 +29,7 @@ PYSOURCES=$(wildcard ${MODULE}/**.py tests/*.py)
 DEVPKGS=-rdev-requirements.txt -rtest-requirements.txt -rmypy-requirements.txt
 DEBDEVPKGS=pep8 python-autopep8 pylint python-coverage pydocstyle sloccount \
 	   python-flake8 python-mock shellcheck
-VERSION=2.5.$(shell TZ=UTC git log --first-parent --max-count=1 \
+VERSION=2.6.$(shell TZ=UTC git log --first-parent --max-count=1 \
 	--format=format:%cd --date=format-local:%Y%m%d%H%M%S)
 
 ## all                    : default task (install in dev mode)
@@ -168,7 +168,7 @@ mypy: $(PYSOURCES)
 	MYPYPATH=$$MYPYPATH:mypy-stubs mypy $^
 
 pyupgrade: $(filter-out schema_salad/metaschema.py,$(PYSOURCES))
-	pyupgrade --exit-zero-even-if-changed --py38-plus $^
+	pyupgrade --exit-zero-even-if-changed --py39-plus $^
 	auto-walrus $^
 
 release-test: FORCE


=====================================
PKG-INFO
=====================================
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: cwltest
-Version: 2.5.20241122133319
+Version: 2.6.20250818005349
 Summary: Common Workflow Language testing framework
 Author-email: Common workflow language working group <common-workflow-language at googlegroups.com>
 License: Apache 2.0
@@ -12,23 +12,23 @@ Classifier: License :: OSI Approved :: Apache Software License
 Classifier: Operating System :: POSIX
 Classifier: Operating System :: MacOS :: MacOS X
 Classifier: Development Status :: 5 - Production/Stable
-Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
 Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
 Classifier: Typing :: Typed
-Requires-Python: <3.14,>=3.8
+Requires-Python: <3.15,>=3.9
 Description-Content-Type: text/x-rst
 License-File: LICENSE
 Requires-Dist: schema-salad<9,>=5.0.20200220195218
 Requires-Dist: junit-xml>=1.8
 Requires-Dist: pytest<9,>=7
 Requires-Dist: defusedxml
-Requires-Dist: importlib_resources>=1.4; python_version < "3.9"
 Provides-Extra: pytest-plugin
 Requires-Dist: pytest; extra == "pytest-plugin"
+Dynamic: license-file
 
 ##########################################
 Common Workflow Language testing framework
@@ -64,7 +64,7 @@ This is a testing tool for checking the output of Tools and Workflows described
 with the Common Workflow Language.  Among other uses, it is used to run the CWL
 conformance tests.
 
-This is written and tested for Python 3.8, 3.9, 3.10, 3.11, 3.12, and 3.13.
+This is written and tested for Python 3.9, 3.10, 3.11, 3.12, and 3.13.
 
 .. contents:: Table of Contents
    :local:


=====================================
README.rst
=====================================
@@ -32,7 +32,7 @@ This is a testing tool for checking the output of Tools and Workflows described
 with the Common Workflow Language.  Among other uses, it is used to run the CWL
 conformance tests.
 
-This is written and tested for Python 3.8, 3.9, 3.10, 3.11, 3.12, and 3.13.
+This is written and tested for Python 3.9, 3.10, 3.11, 3.12, and 3.13.
 
 .. contents:: Table of Contents
    :local:


=====================================
cwltest.egg-info/PKG-INFO
=====================================
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: cwltest
-Version: 2.5.20241122133319
+Version: 2.6.20250818005349
 Summary: Common Workflow Language testing framework
 Author-email: Common workflow language working group <common-workflow-language at googlegroups.com>
 License: Apache 2.0
@@ -12,23 +12,23 @@ Classifier: License :: OSI Approved :: Apache Software License
 Classifier: Operating System :: POSIX
 Classifier: Operating System :: MacOS :: MacOS X
 Classifier: Development Status :: 5 - Production/Stable
-Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
 Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
 Classifier: Typing :: Typed
-Requires-Python: <3.14,>=3.8
+Requires-Python: <3.15,>=3.9
 Description-Content-Type: text/x-rst
 License-File: LICENSE
 Requires-Dist: schema-salad<9,>=5.0.20200220195218
 Requires-Dist: junit-xml>=1.8
 Requires-Dist: pytest<9,>=7
 Requires-Dist: defusedxml
-Requires-Dist: importlib_resources>=1.4; python_version < "3.9"
 Provides-Extra: pytest-plugin
 Requires-Dist: pytest; extra == "pytest-plugin"
+Dynamic: license-file
 
 ##########################################
 Common Workflow Language testing framework
@@ -64,7 +64,7 @@ This is a testing tool for checking the output of Tools and Workflows described
 with the Common Workflow Language.  Among other uses, it is used to run the CWL
 conformance tests.
 
-This is written and tested for Python 3.8, 3.9, 3.10, 3.11, 3.12, and 3.13.
+This is written and tested for Python 3.9, 3.10, 3.11, 3.12, and 3.13.
 
 .. contents:: Table of Contents
    :local:


=====================================
cwltest.egg-info/SOURCES.txt
=====================================
@@ -99,6 +99,7 @@ tests/test_exclude_tags.py
 tests/test_integer_id.py
 tests/test_invalid_outputs.py
 tests/test_multi_lined_doc.py
+tests/test_numeric_id.py
 tests/test_plugin.py
 tests/test_prepare.py
 tests/test_short_names.py


=====================================
cwltest.egg-info/requires.txt
=====================================
@@ -3,8 +3,5 @@ junit-xml>=1.8
 pytest<9,>=7
 defusedxml
 
-[:python_version < "3.9"]
-importlib_resources>=1.4
-
 [pytest-plugin]
 pytest


=====================================
cwltest/_version.py
=====================================
@@ -1,16 +1,34 @@
-# file generated by setuptools_scm
+# file generated by setuptools-scm
 # don't change, don't track in version control
+
+__all__ = [
+    "__version__",
+    "__version_tuple__",
+    "version",
+    "version_tuple",
+    "__commit_id__",
+    "commit_id",
+]
+
 TYPE_CHECKING = False
 if TYPE_CHECKING:
-    from typing import Tuple, Union
+    from typing import Tuple
+    from typing import Union
+
     VERSION_TUPLE = Tuple[Union[int, str], ...]
+    COMMIT_ID = Union[str, None]
 else:
     VERSION_TUPLE = object
+    COMMIT_ID = object
 
 version: str
 __version__: str
 __version_tuple__: VERSION_TUPLE
 version_tuple: VERSION_TUPLE
+commit_id: COMMIT_ID
+__commit_id__: COMMIT_ID
+
+__version__ = version = '2.6.20250818005349'
+__version_tuple__ = version_tuple = (2, 6, 20250818005349)
 
-__version__ = version = '2.5.20241122133319'
-__version_tuple__ = version_tuple = (2, 5, 20241122133319)
+__commit_id__ = commit_id = None


=====================================
cwltest/argparser.py
=====================================
@@ -107,7 +107,8 @@ def arg_parser() -> argparse.ArgumentParser:
     parser.add_argument(
         "--badgedir",
         type=str,
-        help="Create JSON badges and store them in this directory.",
+        help="Create JSON badges, one for each tag (plus a computed 'all' tag) "
+        " and store them in this directory.",
     )
 
     try:


=====================================
cwltest/compare.py
=====================================
@@ -2,7 +2,8 @@
 
 import hashlib
 import json
-from typing import Any, Callable, Dict, Optional, Set
+from typing import Any, Callable, Optional
+
 import cwltest.stdfsaccess
 
 fs_access = cwltest.stdfsaccess.StdFsAccess("")
@@ -26,7 +27,7 @@ class CompareFail(Exception):
 
 
 def _check_keys(
-    keys: Set[str], expected: Dict[str, Any], actual: Dict[str, Any], skip_details: bool
+    keys: set[str], expected: dict[str, Any], actual: dict[str, Any], skip_details: bool
 ) -> None:
     for k in keys:
         try:
@@ -37,7 +38,7 @@ def _check_keys(
             ) from e
 
 
-def _compare_contents(expected: Dict[str, Any], actual: Dict[str, Any]) -> None:
+def _compare_contents(expected: dict[str, Any], actual: dict[str, Any]) -> None:
     with open(actual["path"]) as f:
         actual_contents = f.read()
     if (expected_contents := expected["contents"]) != actual_contents:
@@ -52,7 +53,7 @@ def _compare_contents(expected: Dict[str, Any], actual: Dict[str, Any]) -> None:
 
 
 def _compare_dict(
-    expected: Dict[str, Any], actual: Dict[str, Any], skip_details: bool
+    expected: dict[str, Any], actual: dict[str, Any], skip_details: bool
 ) -> None:
     for c in expected:
         try:
@@ -68,7 +69,7 @@ def _compare_dict(
 
 
 def _compare_directory(
-    expected: Dict[str, Any], actual: Dict[str, Any], skip_details: bool
+    expected: dict[str, Any], actual: dict[str, Any], skip_details: bool
 ) -> None:
     if actual.get("class") != "Directory":
         raise CompareFail.format(
@@ -97,7 +98,7 @@ def _compare_directory(
 
 
 def _compare_file(
-    expected: Dict[str, Any], actual: Dict[str, Any], skip_details: bool
+    expected: dict[str, Any], actual: dict[str, Any], skip_details: bool
 ) -> None:
     _compare_location(expected, actual, skip_details)
     if "contents" in expected:
@@ -117,7 +118,7 @@ def _compare_file(
 
 
 def _compare_location(
-    expected: Dict[str, Any], actual: Dict[str, Any], skip_details: bool
+    expected: dict[str, Any], actual: dict[str, Any], skip_details: bool
 ) -> None:
     if "path" in expected:
         expected_comp = "path"
@@ -160,7 +161,7 @@ def _compare_location(
         )
 
 
-def _compare_checksum(expected: Dict[str, Any], actual: Dict[str, Any]) -> None:
+def _compare_checksum(expected: dict[str, Any], actual: dict[str, Any]) -> None:
     if "path" in actual:
         path = actual["path"]
     else:
@@ -195,7 +196,7 @@ def _compare_checksum(expected: Dict[str, Any], actual: Dict[str, Any]) -> None:
             )
 
 
-def _compare_size(expected: Dict[str, Any], actual: Dict[str, Any]) -> None:
+def _compare_size(expected: dict[str, Any], actual: dict[str, Any]) -> None:
     if "path" in actual:
         path = actual["path"]
     else:


=====================================
cwltest/hooks.py
=====================================
@@ -1,13 +1,13 @@
 """Hooks for pytest-cwl users."""
 
-from typing import Any, Dict, Optional, Tuple
+from typing import Any, Optional
 
 from cwltest import utils
 
 
 def pytest_cwl_execute_test(  # type: ignore[empty-body]
     config: utils.CWLTestConfig, processfile: str, jobfile: Optional[str]
-) -> Tuple[int, Optional[Dict[str, Any]]]:
+) -> tuple[int, Optional[dict[str, Any]]]:
     """
     Execute CWL test using a Python function instead of a command line runner.
 


=====================================
cwltest/main.py
=====================================
@@ -6,12 +6,15 @@ import os
 import sys
 from collections import Counter, defaultdict
 from concurrent.futures import ThreadPoolExecutor
-from typing import Dict, List, Optional, Set, cast
+from typing import Optional, cast
 
 import junit_xml
 import schema_salad.avro
 import schema_salad.ref_resolver
 import schema_salad.schema
+from schema_salad.exceptions import ValidationException
+
+from cwltest import logger, utils
 from cwltest.argparser import arg_parser
 from cwltest.utils import (
     CWLTestConfig,
@@ -19,9 +22,6 @@ from cwltest.utils import (
     TestResult,
     load_optional_fsaccess_plugin,
 )
-from schema_salad.exceptions import ValidationException
-
-from cwltest import logger, utils
 
 if sys.stderr.isatty():
     PREFIX = "\r"
@@ -33,7 +33,7 @@ else:
 
 def _run_test(
     args: argparse.Namespace,
-    test: Dict[str, str],
+    test: dict[str, str],
     test_number: int,
     total_tests: int,
 ) -> TestResult:
@@ -78,8 +78,8 @@ def _run_test(
     return utils.run_test_plain(config, test, test_number)
 
 
-def _expand_number_range(nr: str) -> List[int]:
-    result: List[int] = []
+def _expand_number_range(nr: str) -> list[int]:
+    result: list[int] = []
     for s in nr.split(","):
         sp = s.split("-")
         if len(sp) == 2:
@@ -123,8 +123,8 @@ def main() -> int:
 
     load_optional_fsaccess_plugin()
 
-    ntotal: Dict[str, int] = Counter()
-    npassed: Dict[str, List[CWLTestReport]] = defaultdict(list)
+    ntotal: dict[str, int] = Counter()
+    npassed: dict[str, list[CWLTestReport]] = defaultdict(list)
 
     if args.only_tools:
         alltests = tests
@@ -161,11 +161,11 @@ def main() -> int:
             logger.warning("The `id` field is missing.")
 
     if args.show_tags:
-        alltags: Set[str] = set()
+        alltags: set[str] = set()
         for t in tests:
             ts = t.get("tags", [])
             alltags |= set(ts)
-        for tag in alltags:
+        for tag in sorted(alltags):
             print(tag)
         return 0
 


=====================================
cwltest/plugin.py
=====================================
@@ -3,36 +3,22 @@
 import argparse
 import json
 import os
+import pickle  # nosec
 import time
 import traceback
+from collections.abc import Iterator
 from io import StringIO
 from pathlib import Path
-from typing import (
-    TYPE_CHECKING,
-    Any,
-    Dict,
-    Iterator,
-    List,
-    Optional,
-    Protocol,
-    Set,
-    Tuple,
-    Union,
-    cast,
-)
+from typing import TYPE_CHECKING, Any, Optional, Protocol, Union, cast
 from urllib.parse import urljoin
 
 import pytest
-from cwltest.compare import CompareFail, compare
 
 from cwltest import REQUIRED, UNSUPPORTED_FEATURE, logger, utils
+from cwltest.compare import CompareFail, compare
 
 if TYPE_CHECKING:
-    from _pytest._code.code import ExceptionInfo, TracebackStyle
-    from _pytest.config import Config
-    from _pytest.config import Config as PytestConfig
-    from _pytest.config import PytestPluginManager
-    from _pytest.config.argparsing import Parser as PytestParser
+    from _pytest._code.code import TracebackStyle
     from _pytest.nodes import Node
     from pluggy import HookCaller
 
@@ -42,12 +28,12 @@ class TestRunner(Protocol):
 
     def __call__(
         self, config: utils.CWLTestConfig, processfile: str, jobfile: Optional[str]
-    ) -> List[Optional[Dict[str, Any]]]:
+    ) -> list[Optional[dict[str, Any]]]:
         """Type signature for pytest_cwl_execute_test hook results."""
         ...
 
 
-def _get_comma_separated_option(config: "Config", name: str) -> List[str]:
+def _get_comma_separated_option(config: pytest.Config, name: str) -> list[str]:
     options = config.getoption(name)
     if options is None:
         return []
@@ -58,7 +44,7 @@ def _get_comma_separated_option(config: "Config", name: str) -> List[str]:
 
 
 def _run_test_hook_or_plain(
-    test: Dict[str, str],
+    test: dict[str, str],
     config: utils.CWLTestConfig,
     hook: "HookCaller",
 ) -> utils.TestResult:
@@ -76,7 +62,7 @@ def _run_test_hook_or_plain(
     hook_out = hook(config=config, processfile=processfile, jobfile=jobfile)
     if not hook_out:
         return utils.run_test_plain(config, test)
-    returncode, out = cast(Tuple[int, Optional[Dict[str, Any]]], hook_out[0])
+    returncode, out = cast(tuple[int, Optional[dict[str, Any]]], hook_out[0])
     duration = time.time() - start_time
     outstr = json.dumps(out) if out is not None else "{}"
     if returncode == UNSUPPORTED_FEATURE:
@@ -164,7 +150,7 @@ class CWLItem(pytest.Item):
         self,
         name: str,
         parent: Optional["Node"],
-        spec: Dict[str, Any],
+        spec: dict[str, Any],
     ) -> None:
         """Initialize this CWLItem."""
         super().__init__(name, parent)
@@ -198,7 +184,7 @@ class CWLItem(pytest.Item):
             hook,
         )
         cwl_results = self.config.cwl_results  # type: ignore[attr-defined]
-        cast(List[Tuple[Dict[str, Any], utils.TestResult]], cwl_results).append(
+        cast(list[tuple[dict[str, Any], utils.TestResult]], cwl_results).append(
             (self.spec, result)
         )
         if result.return_code != 0:
@@ -206,7 +192,7 @@ class CWLItem(pytest.Item):
 
     def repr_failure(
         self,
-        excinfo: "ExceptionInfo[BaseException]",
+        excinfo: pytest.ExceptionInfo[BaseException],
         style: Optional["TracebackStyle"] = None,
     ) -> str:
         """
@@ -238,7 +224,7 @@ class CWLItem(pytest.Item):
                 )
             )
 
-    def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
+    def reportinfo(self) -> tuple[Union["os.PathLike[str]", str], Optional[int], str]:
         """Status report."""
         return self.path, 0, "cwl test: %s" % self.name
 
@@ -258,10 +244,10 @@ class CWLYamlFile(pytest.File):
 
     def collect(self) -> Iterator[CWLItem]:
         """Load the cwltest file and yield parsed entries."""
-        include: Set[str] = set(_get_comma_separated_option(self.config, "cwl_include"))
-        exclude: Set[str] = set(_get_comma_separated_option(self.config, "cwl_exclude"))
-        tags: Set[str] = set(_get_comma_separated_option(self.config, "cwl_tags"))
-        exclude_tags: Set[str] = set(
+        include: set[str] = set(_get_comma_separated_option(self.config, "cwl_include"))
+        exclude: set[str] = set(_get_comma_separated_option(self.config, "cwl_exclude"))
+        tags: set[str] = set(_get_comma_separated_option(self.config, "cwl_tags"))
+        exclude_tags: set[str] = set(
             _get_comma_separated_option(self.config, "cwl_exclude_tags")
         )
         tests, _ = utils.load_and_validate_tests(str(self.path))
@@ -302,7 +288,7 @@ class CWLYamlFile(pytest.File):
             yield item
 
 
-__OPTIONS: List[Tuple[str, Dict[str, Any]]] = [
+__OPTIONS: list[tuple[str, dict[str, Any]]] = [
     (
         "--cwl-runner",
         {
@@ -325,7 +311,8 @@ __OPTIONS: List[Tuple[str, Dict[str, Any]]] = [
         "--cwl-badgedir",
         {
             "type": str,
-            "help": "Create badge JSON files and store them in this directory.",
+            "help": "Create badge JSON files, one for each tag (plus a computed "
+            "'all' tag), and store them in this directory.",
         },
     ),
     (
@@ -369,7 +356,7 @@ __OPTIONS: List[Tuple[str, Dict[str, Any]]] = [
 ]
 
 
-def pytest_addoption(parser: "PytestParser") -> None:
+def pytest_addoption(parser: pytest.Parser) -> None:
     """Add our options to the pytest command line."""
     for entry in __OPTIONS:
         parser.addoption(entry[0], **entry[1])
@@ -397,21 +384,65 @@ def pytest_collect_file(
     return None
 
 
-def pytest_configure(config: "PytestConfig") -> None:
+def pytest_configure(config: pytest.Config) -> None:
     """Store the raw tests and the test results."""
-    cwl_results: List[Tuple[Dict[str, Any], utils.TestResult]] = []
+    cwl_results: list[tuple[dict[str, Any], utils.TestResult]] = []
     config.cwl_results = cwl_results  # type: ignore[attr-defined]
 
 
-def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
+def _zip_results(
+    cwl_results: list[tuple[dict[str, Any], utils.TestResult]],
+) -> tuple[list[dict[str, Any]], list[utils.TestResult]]:
+    tests, results = (list(item) for item in zip(*cwl_results))
+    return tests, results
+
+
+def pytest_sessionfinish(session: pytest.Session) -> None:
     """Generate badges."""
+    cwl_badgedir = session.config.getoption("cwl_badgedir")
+    if not cwl_badgedir:
+        return
+
     cwl_results = cast(
-        List[Tuple[Dict[str, Any], utils.TestResult]],
-        getattr(session.config, "cwl_results", None),
+        list[tuple[dict[str, Any], utils.TestResult]],
+        session.config.cwl_results,  # type: ignore[attr-defined]
     )
-    if not cwl_results:
-        return
-    tests, results = (list(item) for item in zip(*cwl_results))
+
+    if session.config.pluginmanager.has_plugin("xdist"):
+        import xdist  # type: ignore[import-untyped]
+
+        directory = cast(
+            pytest.TempPathFactory,
+            session.config._tmp_path_factory,  # type: ignore[attr-defined]
+        ).getbasetemp()
+        if xdist.is_xdist_worker(session):
+            if not cwl_results:
+                return
+            pickle_filename = f"cwltest_{xdist.get_xdist_worker_id(session)}.pickle"
+            with (directory.parent / pickle_filename).open("wb") as handle:
+                pickle.dump(
+                    _zip_results(cwl_results), handle, protocol=pickle.HIGHEST_PROTOCOL
+                )
+            return
+
+        if xdist.is_xdist_controller(session):
+            tests: list[dict[str, Any]] = []
+            results: list[utils.TestResult] = []
+            for pickle_filepath in directory.glob("cwltest_*"):
+                with pickle_filepath.open("rb") as handle:
+                    new_tests, new_results = pickle.load(handle)  # nosec
+                tests.extend(new_tests)
+                results.extend(new_results)
+        else:
+            if not cwl_results:
+                return
+            tests, results = _zip_results(cwl_results)
+
+    else:
+        if not cwl_results:
+            return
+        tests, results = _zip_results(cwl_results)
+
     (
         total,
         passed,
@@ -423,11 +454,11 @@ def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
         nunsupported,
         _,
     ) = utils.parse_results(results, tests)
-    if cwl_badgedir := session.config.getoption("cwl_badgedir"):
-        utils.generate_badges(cwl_badgedir, ntotal, npassed, nfailures, nunsupported)
+
+    utils.generate_badges(cwl_badgedir, ntotal, npassed, nfailures, nunsupported)
 
 
-def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
+def pytest_addhooks(pluginmanager: pytest.PytestPluginManager) -> None:
     """Register our cwl hooks."""
     from cwltest import hooks
 


=====================================
cwltest/utils.py
=====================================
@@ -8,18 +8,10 @@ import sys
 import tempfile
 import time
 from collections import Counter, defaultdict
-from typing import (
-    Any,
-    Dict,
-    Iterable,
-    List,
-    MutableMapping,
-    MutableSequence,
-    Optional,
-    Tuple,
-    Union,
-    cast,
-)
+from collections.abc import Iterable, MutableMapping, MutableSequence
+from importlib.metadata import EntryPoint, entry_points
+from importlib.resources import files
+from typing import Any, Optional, Union, cast
 from urllib.parse import urljoin
 
 import junit_xml
@@ -27,25 +19,14 @@ import ruamel.yaml.scanner
 import schema_salad.avro
 import schema_salad.ref_resolver
 import schema_salad.schema
-import cwltest.compare
-import cwltest.stdfsaccess
-from cwltest.compare import CompareFail, compare
 from rdflib import Graph
 from ruamel.yaml.scalarstring import ScalarString
 from schema_salad.exceptions import ValidationException
 
-if sys.version_info >= (3, 9):
-    from importlib.resources import as_file, files
-else:
-    from importlib_resources import as_file, files
-
-# available since Python 3.8 (minimum version supports as of this
-# writing) so we don't need to fuss with backports
-from importlib.metadata import entry_points, EntryPoint
-
+import cwltest.compare
+import cwltest.stdfsaccess
 from cwltest import REQUIRED, UNSUPPORTED_FEATURE, logger, templock
-
-__all__ = ["files", "as_file"]
+from cwltest.compare import CompareFail, compare
 
 
 class CWLTestConfig:
@@ -61,8 +42,8 @@ class CWLTestConfig:
         outdir: Optional[str] = None,
         classname: Optional[str] = None,
         tool: Optional[str] = None,
-        args: Optional[List[str]] = None,
-        testargs: Optional[List[str]] = None,
+        args: Optional[list[str]] = None,
+        testargs: Optional[list[str]] = None,
         timeout: Optional[int] = None,
         verbose: Optional[bool] = None,
         runner_quiet: Optional[bool] = None,
@@ -77,8 +58,8 @@ class CWLTestConfig:
             self.test_baseuri, os.path.basename(entry) + f"#L{entry_line}"
         )
         self.tool: str = tool or "cwl-runner"
-        self.args: List[str] = args or []
-        self.testargs: List[str] = testargs or []
+        self.args: list[str] = args or []
+        self.testargs: list[str] = testargs or []
         self.timeout: Optional[int] = timeout
         self.verbose: bool = verbose or False
         self.runner_quiet: bool = runner_quiet or True
@@ -88,7 +69,12 @@ class CWLTestReport:
     """Encapsulate relevant test result data for a markdown report."""
 
     def __init__(
-        self, id: str, category: List[str], entry: str, tool: str, job: Optional[str]
+        self,
+        id: Union[int, str],
+        category: list[str],
+        entry: str,
+        tool: str,
+        job: Optional[str],
     ) -> None:
         """Initialize a CWLTestReport object."""
         self.id = id
@@ -124,7 +110,7 @@ class TestResult:
         self.tool = tool
         self.job = job
 
-    def create_test_case(self, test: Dict[str, Any]) -> junit_xml.TestCase:
+    def create_test_case(self, test: dict[str, Any]) -> junit_xml.TestCase:
         """Create a jUnit XML test case from this test result."""
         doc = test.get("doc", "N/A").strip()
         if test.get("tags"):
@@ -144,7 +130,8 @@ class TestResult:
             case.failure_message = self.message
         return case
 
-    def create_report_entry(self, test: Dict[str, Any]) -> CWLTestReport:
+    def create_report_entry(self, test: dict[str, Any]) -> CWLTestReport:
+        """Package test result into a CWLTestReport."""
         return CWLTestReport(
             test.get("id", "no-id"),
             test.get("tags", ["required"]),
@@ -154,7 +141,7 @@ class TestResult:
         )
 
 
-def _clean_ruamel_list(obj: List[Any]) -> Any:
+def _clean_ruamel_list(obj: list[Any]) -> Any:
     """Entrypoint to transform roundtrip loaded ruamel.yaml to plain objects."""
     new_list = []
     for entry in obj:
@@ -188,10 +175,10 @@ def _clean_ruamel(obj: Any) -> Any:
 
 def generate_badges(
     badgedir: str,
-    ntotal: Dict[str, int],
-    npassed: Dict[str, List[CWLTestReport]],
-    nfailures: Dict[str, List[CWLTestReport]],
-    nunsupported: Dict[str, List[CWLTestReport]],
+    ntotal: dict[str, int],
+    npassed: dict[str, list[CWLTestReport]],
+    nfailures: dict[str, list[CWLTestReport]],
+    nunsupported: dict[str, list[CWLTestReport]],
 ) -> None:
     """Generate badges with conformance levels."""
     os.mkdir(badgedir)
@@ -217,10 +204,9 @@ def generate_badges(
 
         with open(f"{badgedir}/{t}.md", "w") as out:
             print(f"# `{t}` tests", file=out)
-
             print("## List of passed tests", file=out)
             for e in npassed[t]:
-                base = f"[{shortname(e.id)}]({e.entry})"
+                base = f"[{shortname(str(e.id))}]({e.entry})"
                 tool = f"[tool]({e.tool})"
                 if e.job:
                     arr = [tool, f"[job]({e.job})"]
@@ -231,7 +217,7 @@ def generate_badges(
 
             print("## List of failed tests", file=out)
             for e in nfailures[t]:
-                base = f"[{shortname(e.id)}]({e.entry})"
+                base = f"[{shortname(str(e.id))}]({e.entry})"
                 tool = f"[tool]({e.tool})"
                 if e.job:
                     arr = [tool, f"[job]({e.job})"]
@@ -242,7 +228,7 @@ def generate_badges(
 
             print("## List of unsupported tests", file=out)
             for e in nunsupported[t]:
-                base = f"[{shortname(e.id)}]({e.entry})"
+                base = f"[{shortname(str(e.id))}]({e.entry})"
                 tool = f"[tool]({e.tool})"
                 if e.job:
                     arr = [tool, f"[job]({e.job})"]
@@ -253,7 +239,7 @@ def generate_badges(
 
 
 def get_test_number_by_key(
-    tests: List[Dict[str, str]], key: str, value: str
+    tests: list[dict[str, str]], key: str, value: str
 ) -> Optional[int]:
     """Retrieve the test index from its name."""
     for i, test in enumerate(tests):
@@ -262,7 +248,7 @@ def get_test_number_by_key(
     return None
 
 
-def load_and_validate_tests(path: str) -> Tuple[Any, Dict[str, Any]]:
+def load_and_validate_tests(path: str) -> tuple[Any, dict[str, Any]]:
     """
     Load and validate the given test file against the cwltest schema.
 
@@ -270,7 +256,7 @@ def load_and_validate_tests(path: str) -> Tuple[Any, Dict[str, Any]]:
     """
     schema_resource = files("cwltest").joinpath("cwltest-schema.yml")
     with schema_resource.open("r", encoding="utf-8") as fp:
-        cache: Optional[Dict[str, Union[str, Graph, bool]]] = {
+        cache: Optional[dict[str, Union[str, Graph, bool]]] = {
             "https://w3id.org/cwl/cwltest/cwltest-schema.yml": fp.read()
         }
     (
@@ -289,30 +275,33 @@ def load_and_validate_tests(path: str) -> Tuple[Any, Dict[str, Any]]:
     tests, metadata = schema_salad.schema.load_and_validate(
         document_loader, avsc_names, path, True
     )
-    tests = cast(List[Dict[str, Any]], _clean_ruamel_list(tests))
+    tests = cast(list[dict[str, Any]], _clean_ruamel_list(tests))
 
     return tests, metadata
 
 
 def parse_results(
     results: Iterable[TestResult],
-    tests: List[Dict[str, Any]],
+    tests: list[dict[str, Any]],
     suite_name: Optional[str] = None,
     report: Optional[junit_xml.TestSuite] = None,
-) -> Tuple[
+) -> tuple[
     int,  # total
     int,  # passed
     int,  # failures
     int,  # unsupported
-    Dict[str, int],  # total for each tag
-    Dict[str, List[CWLTestReport]],  # passed for each tag
-    Dict[str, List[CWLTestReport]],  # failures for each tag
-    Dict[str, List[CWLTestReport]],  # unsupported for each tag
+    dict[str, int],  # total for each tag
+    dict[str, list[CWLTestReport]],  # passed for each tag
+    dict[str, list[CWLTestReport]],  # failures for each tag
+    dict[str, list[CWLTestReport]],  # unsupported for each tag
     Optional[junit_xml.TestSuite],
 ]:
     """
     Parse the results and return statistics and an optional report.
 
+    An additional tag named "all" will be computed, containing all the test
+    results.
+
     Returns the total number of tests, dictionary of test counts
     (total, passed, failed, unsupported) by tag, and a jUnit XML report.
     """
@@ -320,10 +309,10 @@ def parse_results(
     passed = 0
     failures = 0
     unsupported = 0
-    ntotal: Dict[str, int] = Counter()
-    nfailures: Dict[str, List[CWLTestReport]] = defaultdict(list)
-    nunsupported: Dict[str, List[CWLTestReport]] = defaultdict(list)
-    npassed: Dict[str, List[CWLTestReport]] = defaultdict(list)
+    ntotal: dict[str, int] = Counter()
+    nfailures: dict[str, list[CWLTestReport]] = defaultdict(list)
+    nunsupported: dict[str, list[CWLTestReport]] = defaultdict(list)
+    npassed: dict[str, list[CWLTestReport]] = defaultdict(list)
 
     for i, test_result in enumerate(results):
         test_case = test_result.create_test_case(tests[i])
@@ -334,7 +323,7 @@ def parse_results(
             else "cwltest:#{i + 1}"
         )
         total += 1
-        tags = tests[i].get("tags", [])
+        tags = tests[i].get("tags", []) + ["all"]
         for t in tags:
             ntotal[t] += 1
 
@@ -376,12 +365,12 @@ def parse_results(
 
 def prepare_test_command(
     tool: str,
-    args: List[str],
-    testargs: Optional[List[str]],
-    test: Dict[str, Any],
+    args: list[str],
+    testargs: Optional[list[str]],
+    test: dict[str, Any],
     cwd: str,
     quiet: Optional[bool] = True,
-) -> List[str]:
+) -> list[str]:
     """Turn the test into a command line."""
     test_command = [tool]
     test_command.extend(args)
@@ -416,9 +405,9 @@ def prepare_test_command(
 
 
 def prepare_test_paths(
-    test: Dict[str, str],
+    test: dict[str, str],
     cwd: str,
-) -> Tuple[str, Optional[str]]:
+) -> tuple[str, Optional[str]]:
     """Determine the test path and the tool path."""
     cwd = schema_salad.ref_resolver.file_uri(cwd)
     processfile = test["tool"]
@@ -434,13 +423,13 @@ def prepare_test_paths(
 
 def run_test_plain(
     config: CWLTestConfig,
-    test: Dict[str, str],
+    test: dict[str, str],
     test_number: Optional[int] = None,
 ) -> TestResult:
     """Plain test runner."""
-    out: Dict[str, Any] = {}
+    out: dict[str, Any] = {}
     outstr = outerr = ""
-    test_command: List[str] = []
+    test_command: list[str] = []
     duration = 0.0
     number = "?"
 
@@ -509,12 +498,12 @@ def run_test_plain(
             logger.error(
                 """Test %i failed: %s""",
                 test_number,
-                " ".join([shlex.quote(tc) for tc in test_command]),
+                shlex.join(test_command),
             )
         else:
             logger.error(
                 """Test failed: %s""",
-                " ".join([shlex.quote(tc) for tc in test_command]),
+                shlex.join(test_command),
             )
         logger.error(test.get("doc", "").replace("\n", " ").strip())
         if err.returncode == UNSUPPORTED_FEATURE:
@@ -536,7 +525,7 @@ def run_test_plain(
         logger.error(
             """Test %s failed: %s""",
             number,
-            " ".join([shlex.quote(tc) for tc in test_command]),
+            shlex.join(test_command),
         )
         logger.error(outstr)
         logger.error("Parse error %s", str(err))
@@ -545,14 +534,14 @@ def run_test_plain(
         logger.error(
             """Test %s interrupted: %s""",
             number,
-            " ".join([shlex.quote(tc) for tc in test_command]),
+            shlex.join(test_command),
         )
         raise
     except json.JSONDecodeError:
         logger.error(
             """Test %s failed: %s""",
             number,
-            " ".join([shlex.quote(tc) for tc in test_command]),
+            shlex.join(test_command),
         )
         logger.error(test.get("doc", "").replace("\n", " ").strip())
         invalid_json_msg = "Output is not a valid JSON document: '%s'" % outstr
@@ -572,7 +561,7 @@ def run_test_plain(
         logger.error(
             """Test %s timed out: %s""",
             number,
-            " ".join([shlex.quote(tc) for tc in test_command]),
+            shlex.join(test_command),
         )
         logger.error(test.get("doc", "").replace("\n", " ").strip())
         # Kill and re-communicate to get the logs and reap the child, as
@@ -608,7 +597,7 @@ def run_test_plain(
         logger.warning(
             """Test %s failed: %s""",
             number,
-            " ".join([shlex.quote(tc) for tc in test_command]),
+            shlex.join(test_command),
         )
         logger.warning(test.get("doc", "").replace("\n", " ").strip())
         logger.warning("Returned zero but it should be non-zero")
@@ -629,7 +618,7 @@ def run_test_plain(
         logger.warning(
             """Test %s failed: %s""",
             number,
-            " ".join([shlex.quote(tc) for tc in test_command]),
+            shlex.join(test_command),
         )
         logger.warning(test.get("doc", "").replace("\n", " ").strip())
         logger.warning("Compare failure %s", ex)
@@ -675,15 +664,15 @@ def load_optional_fsaccess_plugin() -> None:
     use that to get a filesystem access object that will be used for
     checking test output.
     """
-    fsaccess_eps: List[EntryPoint]
+    fsaccess_eps: list[EntryPoint]
 
     try:
         # The interface to importlib.metadata.entry_points() changed
-        # several times between Python 3.8 and 3.13; the code below
+        # several times between Python 3.9 and 3.13; the code below
         # actually works fine on all of them but there's no single
         # mypy annotation that works across of them.  Explicitly cast
         # it to a consistent type to make mypy shut up.
-        fsaccess_eps = cast(List[EntryPoint], entry_points()["cwltest.fsaccess"])  # type: ignore [redundant-cast, unused-ignore]
+        fsaccess_eps = cast(list[EntryPoint], entry_points()["cwltest.fsaccess"])  # type: ignore [redundant-cast, unused-ignore]
     except KeyError:
         return
 


=====================================
dev-requirements.txt
=====================================
@@ -1,5 +1,5 @@
 diff_cover
-black ~= 24.10
+black ~= 25.1
 pylint
 pep257
 pydocstyle


=====================================
docs/requirements.txt
=====================================
@@ -2,5 +2,4 @@ sphinx >= 2.2
 sphinx-rtd-theme==3.0.2
 sphinx-autoapi
 sphinx-autodoc-typehints
-typed_ast;python_version<'3.8'
 sphinxcontrib-autoprogram


=====================================
mypy-requirements.txt
=====================================
@@ -1,4 +1,4 @@
-mypy==1.13.0
+mypy==1.17.1
 pytest >= 8.3, < 9
 types-setuptools
 types-requests


=====================================
pyproject.toml
=====================================
@@ -1,7 +1,7 @@
 [build-system]
 requires = [
     "setuptools>=61.2",
-    "setuptools_scm>=8.0.4,<9",
+    "setuptools_scm>=8.0.4,<10",
 ]
 build-backend = "setuptools.build_meta"
 
@@ -17,15 +17,15 @@ classifiers = [
     "Operating System :: POSIX",
     "Operating System :: MacOS :: MacOS X",
     "Development Status :: 5 - Production/Stable",
-    "Programming Language :: Python :: 3.8",
     "Programming Language :: Python :: 3.9",
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3.12",
     "Programming Language :: Python :: 3.13",
+    "Programming Language :: Python :: 3.14",
     "Typing :: Typed",
 ]
-requires-python = ">=3.8,<3.14"
+requires-python = ">=3.9,<3.15"
 dynamic = ["version", "dependencies"]
 
 [project.readme]


=====================================
requirements.txt
=====================================
@@ -2,4 +2,3 @@ schema-salad >= 5.0.20200220195218, < 9
 junit-xml >= 1.8
 pytest >= 7, < 9
 defusedxml
-importlib_resources>=1.4;python_version<'3.9'


=====================================
tests/test_badgedir.py
=====================================
@@ -1,5 +1,5 @@
-import os
 import json
+import os
 from pathlib import Path
 from textwrap import dedent
 
@@ -53,6 +53,15 @@ def test_badgedir(tmp_path: Path) -> None:
         assert obj.get("color", "") == "yellow"
     assert (badgedir / "command_line_tool.md").exists()
 
+    all_tests = badgedir / "all.json"
+    assert all_tests.exists()
+    with open(all_tests) as file:
+        obj = json.load(file)
+        assert obj.get("subject", "") == "all"
+        assert obj.get("status", "") == "0%"
+        assert obj.get("color", "") == "yellow"
+    assert (badgedir / "all.md").exists()
+
 
 def test_badgedir_report_with_baseuri(tmp_path: Path) -> None:
     badgedir = tmp_path / "badgedir"


=====================================
tests/test_categories.py
=====================================
@@ -3,8 +3,6 @@ import re
 from os import linesep as n
 from os import sep as p
 from pathlib import Path
-from typing import cast
-from xml.etree.ElementTree import Element
 
 import defusedxml.ElementTree as ET
 import schema_salad.ref_resolver
@@ -88,8 +86,28 @@ def test_category_in_junit_xml(tmp_path: Path) -> None:
     ]
     run_with_mock_cwl_runner(args)
     tree = ET.parse(junit_xml_report)
-    root = tree.getroot()
-    category = cast(
-        Element, cast(Element, root.find("testsuite")).find("testcase")
-    ).attrib["class"]
+    assert (root := tree.getroot()) is not None
+    assert (testsuite_el := root.find("testsuite")) is not None
+    assert (testcase_el := testsuite_el.find("testcase")) is not None
+    category = testcase_el.attrib["class"]
     assert category == "js, init_work_dir"
+
+
+def test_list_all_tags() -> None:
+    args = [
+        "--test",
+        schema_salad.ref_resolver.file_uri(
+            get_data("tests/test-data/conformance_test_v1.2.cwltest.yaml")
+        ),
+        "--show-tags",
+    ]
+    error_code, stdout, stderr = run_with_mock_cwl_runner(args)
+    assert error_code == 0, stderr
+    assert (
+        stdout
+        == """command_line_tool
+expression_tool
+inline_javascript
+required
+"""
+    )


=====================================
tests/test_compare.py
=====================================
@@ -1,8 +1,9 @@
 import os
 from pathlib import Path
-from typing import Any, Dict
+from typing import Any
 
 import pytest
+
 from cwltest.compare import CompareFail, _compare_directory, _compare_file, compare
 
 from .util import get_data
@@ -10,7 +11,7 @@ from .util import get_data
 
 def test_compare_any_success() -> None:
     expected = "Any"
-    actual: Dict[str, Any] = {}
+    actual: dict[str, Any] = {}
     compare(expected, actual)
 
 
@@ -385,7 +386,7 @@ def test_compare_file_failure_none() -> None:
         "class": "File",
         "checksum": "sha1$7448d8798a4380162d4b56f9b452e2f6f9e24e7b",
     }
-    actual: Dict[str, Any] = {}
+    actual: dict[str, Any] = {}
     with pytest.raises(CompareFail):
         compare(expected, actual)
 
@@ -554,7 +555,7 @@ def test_compare_list_failure_type() -> None:
             "10",
         ]
     }
-    actual: Dict[str, Any] = {"args": {}}
+    actual: dict[str, Any] = {"args": {}}
     with pytest.raises(CompareFail):
         compare(expected, actual)
 


=====================================
tests/test_numeric_id.py
=====================================
@@ -0,0 +1,15 @@
+from os import linesep as n
+
+from .util import get_data, run_with_mock_cwl_runner
+
+
+def test_include_by_number() -> None:
+    args = [
+        "--test",
+        get_data("tests/test-data/exclude-tags.yml"),
+        "-n1",
+    ]
+    error_code, stdout, stderr = run_with_mock_cwl_runner(args)
+    assert f"[1/3] opt-error1: Test with label{n}" in stderr
+    assert "opt-error2" not in stderr
+    assert "opt-error3" not in stderr


=====================================
tests/test_plugin.py
=====================================
@@ -1,12 +1,10 @@
 import os
 import shutil
 from pathlib import Path
-from typing import TYPE_CHECKING
 
-from .util import get_data
+import pytest
 
-if TYPE_CHECKING:
-    from _pytest.pytester import Pytester
+from .util import get_data
 
 
 def _load_v1_0_dir(path: Path) -> None:
@@ -20,7 +18,7 @@ def _load_v1_0_dir(path: Path) -> None:
     shutil.copy(get_data("tests/test-data/v1.0/args.py"), inner_dir)
 
 
-def test_include(pytester: "Pytester") -> None:
+def test_include(pytester: pytest.Pytester) -> None:
     """Test the pytest plugin using cwltool as cwl-runner."""
     path = pytester.copy_example("conformance_test_v1.0.cwltest.yml")
     shutil.copy(
@@ -36,7 +34,7 @@ def test_include(pytester: "Pytester") -> None:
     result.assert_outcomes(passed=1, skipped=1)
 
 
-def test_exclude(pytester: "Pytester") -> None:
+def test_exclude(pytester: pytest.Pytester) -> None:
     """Test the pytest plugin using cwltool as cwl-runner."""
     path = pytester.copy_example("conformance_test_v1.0.cwltest.yml")
     shutil.copy(
@@ -52,7 +50,7 @@ def test_exclude(pytester: "Pytester") -> None:
     result.assert_outcomes(passed=0, skipped=2)
 
 
-def test_tags(pytester: "Pytester") -> None:
+def test_tags(pytester: pytest.Pytester) -> None:
     """Test the pytest plugin using cwltool as cwl-runner."""
     path = pytester.copy_example("conformance_test_v1.0.cwltest.yml")
     shutil.copy(
@@ -65,7 +63,7 @@ def test_tags(pytester: "Pytester") -> None:
     result.assert_outcomes(passed=1, skipped=1)
 
 
-def test_exclude_tags(pytester: "Pytester") -> None:
+def test_exclude_tags(pytester: pytest.Pytester) -> None:
     """Test the pytest plugin using cwltool as cwl-runner."""
     path = pytester.copy_example("conformance_test_v1.0.cwltest.yml")
     shutil.copy(
@@ -81,21 +79,52 @@ def test_exclude_tags(pytester: "Pytester") -> None:
     result.assert_outcomes(skipped=2)
 
 
-def test_badgedir(pytester: "Pytester") -> None:
+def test_badgedir(pytester: pytest.Pytester) -> None:
     """Test the pytest plugin creates the badges directory."""
     path = pytester.copy_example("conformance_test_v1.0.cwltest.yml")
     shutil.copy(
         get_data("tests/test-data/cwltool-conftest.py"), path.parent / "conftest.py"
     )
     _load_v1_0_dir(path)
-    assert not os.path.exists("cwl-badges")
-    pytester.runpytest(
-        "-k", "conformance_test_v1.0.cwltest.yml", "--cwl-badgedir", "cwl-badges"
+    badge_path = path.parent / "cwl-badges"
+    assert not badge_path.exists()
+    result = pytester.runpytest_inprocess(
+        "-k", "conformance_test_v1.0.cwltest.yml", "--cwl-badgedir", str(badge_path)
+    )
+    result.assert_outcomes(passed=2)
+    assert badge_path.exists()
+    assert (badge_path / "command_line_tool.json").exists()
+    assert (badge_path / "command_line_tool.md").exists()
+    assert (badge_path / "required.json").exists()
+    assert (badge_path / "required.md").exists()
+
+
+def test_badgedir_xdist(pytester: pytest.Pytester) -> None:
+    """Test the pytest plugin creates the badges directory even with xdist."""
+    path = pytester.copy_example("conformance_test_v1.0.cwltest.yml")
+    shutil.copy(
+        get_data("tests/test-data/cwltool-conftest.py"), path.parent / "conftest.py"
+    )
+    _load_v1_0_dir(path)
+    badge_path = path.parent / "cwl-badges"
+    assert not badge_path.exists()
+    result = pytester.runpytest_inprocess(
+        "-n",
+        "2",
+        "-k",
+        "conformance_test_v1.0.cwltest.yml",
+        "--cwl-badgedir",
+        str(badge_path),
     )
-    assert os.path.exists("cwl-badges")
+    result.assert_outcomes(passed=2)
+    assert badge_path.exists()
+    assert (badge_path / "command_line_tool.json").exists()
+    assert (badge_path / "command_line_tool.md").exists()
+    assert (badge_path / "required.json").exists()
+    assert (badge_path / "required.md").exists()
 
 
-def test_no_label(pytester: "Pytester") -> None:
+def test_no_label(pytester: pytest.Pytester) -> None:
     """Test the pytest plugin correctly extracts test names from the id field when label is missing."""
     path = pytester.copy_example("conformance_test_v1.2.cwltest.yaml")
     shutil.copy(
@@ -108,7 +137,7 @@ def test_no_label(pytester: "Pytester") -> None:
     result.assert_outcomes(passed=2, skipped=1)
 
 
-def test_cwltool_hook(pytester: "Pytester") -> None:
+def test_cwltool_hook(pytester: pytest.Pytester) -> None:
     """Test the pytest plugin using cwltool as cwl-runner."""
     path = pytester.copy_example("conformance_test_v1.0.cwltest.yml")
     shutil.copy(
@@ -119,7 +148,7 @@ def test_cwltool_hook(pytester: "Pytester") -> None:
     result.assert_outcomes(passed=2)
 
 
-def test_no_hook(pytester: "Pytester") -> None:
+def test_no_hook(pytester: pytest.Pytester) -> None:
     """Test the pytest plugin using the default cwl-runner."""
     path = pytester.copy_example("conformance_test_v1.0.cwltest.yml")
     _load_v1_0_dir(path)


=====================================
tests/test_short_names.py
=====================================
@@ -1,7 +1,5 @@
 from os import linesep as n
 from pathlib import Path
-from typing import cast
-from xml.etree.ElementTree import Element
 
 import defusedxml.ElementTree as ET
 
@@ -49,8 +47,8 @@ def test_short_name_in_junit_xml(tmp_path: Path) -> None:
     ]
     run_with_mock_cwl_runner(args)
     tree = ET.parse(junit_xml_report)
-    root = tree.getroot()
-    category = cast(
-        Element, cast(Element, root.find("testsuite")).find("testcase")
-    ).attrib["file"]
+    assert (root := tree.getroot()) is not None
+    assert (testsuite_el := root.find("testsuite")) is not None
+    assert (testcase_el := testsuite_el.find("testcase")) is not None
+    category = testcase_el.attrib["file"]
     assert category == "opt-error"


=====================================
tests/test_timeout.py
=====================================
@@ -1,7 +1,5 @@
 import os
 from pathlib import Path
-from typing import cast
-from xml.etree.ElementTree import Element
 
 import defusedxml.ElementTree as ET
 import schema_salad.ref_resolver
@@ -31,20 +29,14 @@ def test_timeout_stderr_stdout(tmp_path: Path) -> None:
     assert "Test 1 timed out" in stderr
     tree = ET.parse(junit_xml_report)
     try:
-        root = tree.getroot()
-        timeout_text = cast(
-            Element,
-            cast(Element, cast(Element, root.find("testsuite")).find("testcase")).find(
-                "failure"
-            ),
-        ).text
-        timeout_stderr = cast(
-            Element,
-            cast(Element, cast(Element, root.find("testsuite")).find("testcase")).find(
-                "system-err"
-            ),
-        ).text
+        assert (root := tree.getroot()) is not None
+        assert (testsuite_el := root.find("testsuite")) is not None
+        assert (testcase_el := testsuite_el.find("testcase")) is not None
+        assert (failure_el := testcase_el.find("failure")) is not None
+        timeout_text = failure_el.text
         assert timeout_text is not None and "Test timed out" in timeout_text
+        assert (system_err_el := testcase_el.find("system-err")) is not None
+        timeout_stderr = system_err_el.text
         assert timeout_stderr is not None and "timeout stderr" in timeout_stderr
     except AttributeError as e:
         print(junit_xml_report.read_text())


=====================================
tests/util.py
=====================================
@@ -4,10 +4,9 @@ import atexit
 import os
 import subprocess  # nosec
 from contextlib import ExitStack
+from importlib.resources import as_file, files
 from pathlib import Path
-from typing import List, Optional, Tuple
-
-from cwltest.utils import as_file, files
+from typing import Optional
 
 
 def get_data(filename: str) -> str:
@@ -29,8 +28,8 @@ def get_data(filename: str) -> str:
 
 
 def run_with_mock_cwl_runner(
-    args: List[str], cwl_runner: Optional[str] = None
-) -> Tuple[int, str, str]:
+    args: list[str], cwl_runner: Optional[str] = None
+) -> tuple[int, str, str]:
     """Bind a mock cwlref-runner implementation to cwltest."""
     if cwl_runner is None:
         cwl_runner = get_data("tests/test-data/mock_cwl_runner.py")


=====================================
tox.ini
=====================================
@@ -1,9 +1,9 @@
 [tox]
 envlist =
-  py3{8,9,10,11,12,13}-lint,
-  py3{8,9,10,11,12,13}-unit,
-  py3{8,9,10,11,12,13}-bandit,
-  py3{8,9,10,11,12,13}-mypy,
+  py3{9,10,11,12,13,14}-lint,
+  py3{9,10,11,12,13,14}-unit,
+  py3{9,10,11,12,13,14}-bandit,
+  py3{9,10,11,12,13,14}-mypy,
   py312-lintreadme,
   py312-pydocstyle
 
@@ -17,63 +17,63 @@ testpaths = tests
 
 [gh-actions]
 python =
-  3.8: py38
   3.9: py39
   3.10: py310
   3.11: py311
   3.12: py312
   3.13: py313
+  3.14: py314
 
 [testenv]
 skipsdist =
-  py3{8,9,10,11,12,13}-!{unit,mypy,lintreadme} = True
+  py3{9,10,11,12,13,14}-!{unit,mypy,lintreadme} = True
 
 description =
-  py3{8,9,10,11,12,13}-unit: Run the unit tests
-  py3{8,9,10,11,12,13}-lint: Lint the Python code
-  py3{8,9,10,11,12,13}-bandit: Search for common security issues
-  py3{8,9,10,11,12,13}-mypy: Check for type safety
-  py312-pydocstyle: docstring style checker
-  py312-lintreadme: Lint the README.rst->.md conversion
+  py3{9,10,11,12,13,14}-unit: Run the unit tests
+  py3{9,10,11,12,13,14}-lint: Lint the Python code
+  py3{9,10,11,12,13,14}-bandit: Search for common security issues
+  py3{9,10,11,12,13,14}-mypy: Check for type safety
+  py313-pydocstyle: docstring style checker
+  py313-lintreadme: Lint the README.rst->.md conversion
 
 passenv =
   CI
   GITHUB_*
 
 deps =
-  py3{8,9,10,11,12,13}-{unit,mypy}: -rrequirements.txt
-  py3{8,9,10,11,12,13}-{unit,mypy}: -rtest-requirements.txt
-  py3{8,9,10,11,12,13}-lint: flake8-bugbear
-  py3{8,9,10,11,12,13}-lint: black~=23.1
-  py3{8,9,10,11,12,13}-bandit: bandit
-  py3{8,9,10,11,12,13}-mypy: -rmypy-requirements.txt
+  py3{9,10,11,12,13,14}-{unit,mypy}: -rrequirements.txt
+  py3{9,10,11,12,13,14}-{unit,mypy}: -rtest-requirements.txt
+  py3{9,10,11,12,13,14}-lint: flake8-bugbear
+  py3{9,10,11,12,13,14}-lint: black~=23.1
+  py3{9,10,11,12,13,14}-bandit: bandit
+  py3{9,10,11,12,13,14}-mypy: -rmypy-requirements.txt
 
 set_env =
-  py3{8,3,10,11,12,13}-unit: LC_ALL = C.UTF-8
+  py3{9,10,11,12,13,14}-unit: LC_ALL = C.UTF-8
   COV_CORE_SOURCE=cwltest
   COV_CORE_CONFIG={toxinidir}/.coveragerc
   COV_CORE_DATAFILE={toxinidir}/.coverage.eager
 
 commands =
-  py3{8,9,10,11,12,13}-unit: python -m pip install -U pip setuptools wheel
-  py3{8,9,10,11,12,13}-unit: python -m pytest --cov --cov-config={toxinidir}/.coveragerc --cov-append {posargs}
-  py3{8,9,10,11,12,13}-unit: coverage xml
-  py3{8,9,10,11,12,13}-bandit: bandit --recursive cwltest
-  py3{8,9,10,11,12,13}-lint: make flake8
-  py3{8,9,10,11,12,13}-lint: make format-check
-  py3{8,9,10,11,12,13}-mypy: make mypy
+  py3{9,10,11,12,13,14}-unit: python -m pip install -U pip setuptools wheel
+  py3{9,10,11,12,13,14}-unit: python -m pytest --cov --cov-config={toxinidir}/.coveragerc --cov-append {posargs}
+  py3{9,10,11,12,13,14}-unit: coverage xml
+  py3{9,10,11,12,13,14}-bandit: bandit --recursive cwltest
+  py3{9,10,11,12,13,14}-lint: make flake8
+  py3{9,10,11,12,13,14}-lint: make format-check
+  py3{9,10,11,12,13,14}-mypy: make mypy
 
 allowlist_externals =
-  py3{8,9,10,11,12,13}-lint: flake8
-  py3{8,9,10,11,12,13}-lint: black
-  py3{8,9,10,11,12,13}-{mypy,shellcheck,lint,unit}: make
+  py3{9,10,11,12,13,14}-lint: flake8
+  py3{9,10,11,12,13,14}-lint: black
+  py3{9,10,11,12,13,14}-{mypy,shellcheck,lint,unit}: make
 
 skip_install =
-  py3{8,9,10,11,12,13}-lint: true
-  py3{8,9,10,11,12,13}-bandit: true
+  py3{9,10,11,12,13,14}-lint: true
+  py3{9,10,11,12,13,14}-bandit: true
 
 
-[testenv:py312-pydocstyle]
+[testenv:py313-pydocstyle]
 allowlist_externals = make
 commands = make diff_pydocstyle_report
 deps =
@@ -81,7 +81,7 @@ deps =
     diff-cover
 skip_install = true
 
-[testenv:py312-lintreadme]
+[testenv:py313-lintreadme]
 description = Lint the README.rst->.md conversion
 commands =
   python -m build --outdir dist



View it on GitLab: https://salsa.debian.org/med-team/cwltest/-/commit/68eb88a7ac008577769f8ea18f2fc092fe334e85

-- 
View it on GitLab: https://salsa.debian.org/med-team/cwltest/-/commit/68eb88a7ac008577769f8ea18f2fc092fe334e85
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/debian-med-commit/attachments/20250930/be38e9e8/attachment-0001.htm>


More information about the debian-med-commit mailing list