././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1793979163.624378 cwltest-2.7.20261106145043/0000775000175000017500000000000015273371434014271 5ustar00michaelmichael././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3923154 cwltest-2.7.20261106145043/.coveragerc0000664000175000017500000000060715273371155016415 0ustar00michaelmichael[paths] source = cwltest */site-packages/cwltest */cwltest [run] branch = True source_pkgs = cwltest omit = tests/* */site-packages/cwltest/tests/* [report] exclude_lines = if self.debug: pragma: no cover raise NotImplementedError if __name__ == .__main__.: if TYPE_CHECKING: \.\.\. ignore_errors = True omit = tests/* */site-packages/cwltest/tests/* ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3923154 cwltest-2.7.20261106145043/.flake80000664000175000017500000000024415273371155015444 0ustar00michaelmichael[flake8] ignore = E203,W503 max-line-length = 100 select = B,C,E,F,W,T4 extend-ignore = E501 extend-select = B9 per-file-ignores= ./tests/test_badgedir.py:B950 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3923154 cwltest-2.7.20261106145043/.git-blame-ignore-revs0000664000175000017500000000006615273371155020373 0ustar00michaelmichael# black 23.1 343562963673af2b239f773c89ede956482d6c2f ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793979163.6141083 cwltest-2.7.20261106145043/.github/0000775000175000017500000000000015273371434015631 5ustar00michaelmichael././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3923154 cwltest-2.7.20261106145043/.github/dependabot.yml0000664000175000017500000000124315273371155020461 0ustar00michaelmichael# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "pip" directory: "/" # Location of package manifests schedule: interval: "daily" cooldown: default-days: 7 # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" cooldown: default-days: 7 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1793979163.614304 cwltest-2.7.20261106145043/.github/workflows/0000775000175000017500000000000015273371434017666 5ustar00michaelmichael././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1793978989.394919 cwltest-2.7.20261106145043/.github/workflows/ci-tests.yml0000664000175000017500000000716515273371155022155 0ustar00michaelmichaelname: Continuous integration tests on: push: branches: [ main ] pull_request: branches: [ main ] concurrency: group: build-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true permissions: {} jobs: tox: name: CI tests via Tox 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: [10, 11, 12, 13, 14, 15] step: [lint, unit, mypy, bandit] env: py-semver: ${{ format('{0}.{1}', matrix.py-ver-major, matrix.py-ver-minor) }} TOXENV: ${{ format('py{0}{1}-{2}', matrix.py-ver-major, matrix.py-ver-minor, matrix.step) }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - name: Set up Python uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: ${{ env.py-semver }} allow-prereleases: true cache: pip - name: Upgrade setuptools and install tox run: | pip install -U pip setuptools wheel pip install "tox>4,<5" "tox-gh-actions>3" - name: install dev libraries run: sudo apt-get update && sudo apt-get install -y libxml2-dev libxslt-dev - name: MyPy cache if: ${{ matrix.step == 'mypy' }} uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .mypy_cache/${{ env.py-semver }} key: mypy-${{ env.py-semver }} - name: Test with tox run: tox - name: Upload coverage to Codecov if: ${{ matrix.step == 'unit' }} uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} tox-style: name: CI linters via Tox runs-on: ubuntu-24.04 strategy: matrix: step: [lintreadme, pydocstyle] env: py-semver: "3.14" TOXENV: ${{ format('py314-{0}', matrix.step) }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - name: Set up Python uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: ${{ env.py-semver }} cache: pip - name: Upgrade setuptools and install tox run: | pip install -U pip setuptools wheel pip install "tox>4,<5" "tox-gh-actions>3" - if: ${{ matrix.step == 'pydocstyle' && github.event_name == 'pull_request'}} name: Create local branch for diff-quality for PRs run: git branch "${GITHUB_BASE_REF}" "origin/${GITHUB_BASE_REF}" env: GITHUB_BASE_REF: ${{ github.base_ref }} - name: Test with tox run: tox release_test: name: cwltest release test runs-on: ubuntu-24.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: "3.14" cache: pip - name: Install packages run: | pip install -U pip setuptools wheel pip install virtualenv - name: Release test env: RELEASE_SKIP: head run: ./release-test.sh ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3949723 cwltest-2.7.20261106145043/.github/workflows/zizmor.yml0000664000175000017500000000117215273371155021744 0ustar00michaelmichaelname: GitHub Actions Security Analysis with zizmor 🌈 on: push: branches: ["main"] pull_request: branches: ["**"] permissions: {} jobs: zizmor: name: Run zizmor 🌈 runs-on: ubuntu-latest permissions: security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files. steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Run zizmor 🌈 uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3949723 cwltest-2.7.20261106145043/.gitignore0000664000175000017500000000024715273371155016264 0ustar00michaelmichael*.pyc .eggs .idea .tox *.egg-info build dist # Generated by tox coverage.xml .coverage .coverage.* pydocstyle_report.txt # Generated by `make release-test` testenv* ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3949723 cwltest-2.7.20261106145043/.readthedocs.yml0000664000175000017500000000106015273371155017354 0ustar00michaelmichael# .readthedocs.yml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py # Optionally build your docs in additional formats such as PDF and ePub formats: all build: os: ubuntu-24.04 tools: python: "3.11" apt_packages: - graphviz python: install: - requirements: docs/requirements.txt - method: pip path: . extra_requirements: - pytest-plugin ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3949723 cwltest-2.7.20261106145043/CONTRIBUTING.md0000664000175000017500000000274715273371155016534 0ustar00michaelmichael# Coding agents For copyright and licensing reasons we can't accept any contributions made with Claude or other LLM based coding agents. You can use whatever tools you want for code exploration, but we can only accept human written code for the CWL project. # Style guide - Python 3.10+ compatible code - PEP-484 type hints - Prefer new style format strings https://pyformat.info/ - Use ``make format`` to format your code It is suggested that you run `git config blame.ignoreRevsFile .git-blame-ignore-revs` to filter out mass-formatting commits from `git blame`. # Installing from source - Clone the Git repository: `git clone https://github.com/common-workflow-language/cwltest.git` - Switch to repository directory: `cd cwltest` - Create a virtual environment: `python3 -m venv env` - To begin using the virtual environment, it needs to be activated: `source env/bin/activate` - To check if you have the virtual environment set up: `which python` and it should point to python executable in your virtual env - Install the project in editable mode along with the development dependencies: `make dev` - After you've made the changes, you can the complete test suite via tox: ``tox`` - If you want to run specific tests, say ``unit tests`` in Python 3.13, then: ``tox run -e py313-unit``. - Look at ``tox -l`` for all available tests and runtimes - If tests are passing, you can create a PR on the GitHub repository. - After you're done working you can deactivate the virtual environment: ``deactivate`` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3949723 cwltest-2.7.20261106145043/LICENSE0000664000175000017500000002613515273371155015305 0ustar00michaelmichael Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3949723 cwltest-2.7.20261106145043/MANIFEST.in0000664000175000017500000000052415273371155016030 0ustar00michaelmichaelinclude Makefile tox.ini mypy.ini include .flake8 .coveragerc release-test.sh include docs/Makefile docs/conf.py docs/requirements.txt docs/_static/favicon.ico docs/*.rst include cwltest/*.yml include tests/* include tests/test-data/* include tests/test-data/v1.0/* recursive-include mypy-stubs *.py? global-exclude *~ global-exclude *.pyc ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3949723 cwltest-2.7.20261106145043/Makefile0000664000175000017500000001430515273371155015734 0ustar00michaelmichael# This file is part of cwltest, # https://github.com/common-workflow-language/cwltest/, and is # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # Contact: common-workflow-language@googlegroups.com # make format to fix most python formatting errors # make pylint to check Python code for enhanced compliance including naming # and documentation # make coverage-report to check coverage of the python scripts by the tests MODULE=cwltest PACKAGE=cwltest # `SHELL=bash` doesn't work for some, so don't use BASH-isms like # `[[` conditional expressions. PYSOURCES=$(wildcard src/${MODULE}/**.py tests/*.py) DEBDEVPKGS=pep8 python-autopep8 pylint python-coverage pydocstyle sloccount \ python-flake8 python-mock shellcheck VERSION=2.7.$(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) all: dev ## help : print this help message and exit help: Makefile @sed -n 's/^##//p' $< ## cleanup : shortcut for "make sort_imports format flake8 diff_pydocstyle_report" cleanup: sort_imports format flake8 diff_pydocstyle_report ## install-dep : install most of the development dependencies via pip install-dep: install-dependencies install-dependencies: FORCE python3 -m pip install -U pip setuptools wheel build python3 -m pip install --upgrade --group dev ## install-deb-dep : install most of the dev dependencies via apt-get install-deb-dep: sudo apt-get install $(DEBDEVPKGS) ## install : install the cwltest package and cwltest script install: FORCE python3 -m pip install . ## dev : install the cwltest package in dev mode dev: install-dep python3 -m pip install -e . ## dist : create a module package for distribution dist: dist/${MODULE}-$(VERSION).tar.gz dist/${MODULE}-$(VERSION).tar.gz: $(SOURCES) python3 -m build ## docs : make the docs docs: FORCE cd docs && $(MAKE) html ## clean : clean up all temporary / machine-generated files clean: FORCE find src -name '*.pyc' -delete rm -Rf .coverage\.* .coverage rm -f diff-cover.html # Linting and code style related targets ## sort_import : sorting imports using isort: https://github.com/timothycrosley/isort sort_imports: $(PYSOURCES) mypy-stubs isort $^ remove_unused_imports: $(filter-out schema_salad/metaschema.py,$(PYSOURCES)) autoflake --in-place --remove-all-unused-imports $^ pep257: pydocstyle ## pydocstyle : check Python docstring style pydocstyle: $(PYSOURCES) pydocstyle --add-ignore=D100,D101,D102,D103 $^ || true pydocstyle_report.txt: $(PYSOURCES) pydocstyle $^ > $@ 2>&1 || true ## diff_pydocstyle_report : check Python docstring style for changed files only diff_pydocstyle_report: pydocstyle_report.txt diff-quality --compare-branch=main --violations=pydocstyle --fail-under=100 $^ ## codespell : check for common misspellings codespell: codespell -w $(shell git ls-files | grep -v mypy-stubs | grep -v gitignore) ## format : check/fix all code indentation and formatting (runs black) format: $(PYSOURCES) mypy-stubs black $^ format-check: $(PYSOURCES) mypy-stubs black --diff --check $^ ## pylint : run static code analysis on Python code pylint: $(PYSOURCES) pylint --msg-template="{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}" \ $^ -j0|| true pylint_report.txt: $(PYSOURCES) pylint --msg-template="{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}" \ $^ -j0> $@ || true diff_pylint_report: pylint_report.txt diff-quality --compare-branch=main --violations=pylint pylint_report.txt .coverage: $(PYSOURCES) COV_CORE_SOURCE=src/cwltest COV_CORE_CONFIG=.coveragerc COV_CORE_DATAFILE=.coverage \ python3 -m pytest --cov --cov-append --cov-report= # https://pytest-cov.readthedocs.io/en/latest/plugins.html#plugin-coverage coverage.xml: .coverage coverage xml coverage.html: htmlcov/index.html htmlcov/index.html: .coverage coverage html @echo Test coverage of the Python code is now in htmlcov/index.html coverage-report: .coverage coverage report diff-cover: coverage.xml diff-cover --compare-branch=main $^ diff-cover.html: coverage.xml diff-cover --compare-branch=main $^ --html-report $@ ## test : run the cwltest test suite test: $(PYSOURCES) python3 -m pytest -rs ${PYTEST_EXTRA} ## testcov : run the cwltest test suite and collect coverage testcov: $(PYSOURCES) pytest --cov ${PYTEST_EXTRA} sloccount.sc: $(PYSOURCES) Makefile sloccount --duplicates --wide --details $^ > $@ ## sloccount : count lines of code sloccount: $(PYSOURCES) Makefile sloccount $^ list-author-emails: @echo 'name, E-Mail Address' @git log --format='%aN,%aE' | sort -u | grep -v 'root' mypy3: mypy mypy: $(PYSOURCES) MYPYPATH=$$MYPYPATH:mypy-stubs mypy $^ pyupgrade: $(filter-out schema_salad/metaschema.py,$(PYSOURCES)) pyupgrade --exit-zero-even-if-changed --py310-plus $^ auto-walrus $^ release-test: FORCE git diff-index --quiet HEAD -- || ( echo You have uncommitted changes, please commit them and try again; false ) ./release-test.sh release: export SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION} && \ ./release-test.sh && \ . testenv2/bin/activate && \ python3 -m pip install build && \ python3 -m build testenv2/src/${PACKAGE} && \ python3 -m pip install twine && \ twine upload testenv2/src/${PACKAGE}/dist/* && \ git tag --no-sign ${VERSION} && git push --tags flake8: FORCE flake8 $(PYSOURCES) FORCE: # Use this to print the value of a Makefile variable # Example `make print-VERSION` # From https://www.cmcrossroads.com/article/printing-value-makefile-variable print-% : ; @echo $* = $($*) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793979163.6243417 cwltest-2.7.20261106145043/PKG-INFO0000644000175000017500000001334515273371434015372 0ustar00michaelmichaelMetadata-Version: 2.4 Name: cwltest Version: 2.7.20261106145043 Summary: Common Workflow Language testing framework Author-email: Common workflow language working group License: Apache 2.0 Project-URL: Homepage, https://github.com/common-workflow-language/cwltest Classifier: Environment :: Console Classifier: Framework :: Pytest 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.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: Programming Language :: Python :: 3.15 Classifier: Typing :: Typed Requires-Python: <3.16,>=3.10 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: schema-salad<9,>=5.0.20200220195218 Requires-Dist: junit-xml>=1.8 Provides-Extra: pytest-plugin Requires-Dist: pytest; extra == "pytest-plugin" Dynamic: license-file ########################################## Common Workflow Language testing framework ########################################## |Linux Build Status| |Code coverage| PyPI: |PyPI Version| |PyPI Downloads Month| |Total PyPI Downloads| Conda: |Conda Version| |Conda Installs| .. |Linux Build Status| image:: https://github.com/common-workflow-language/cwltest/actions/workflows/ci-tests.yml/badge.svg?branch=main :target: https://github.com/common-workflow-language/cwltest/actions/workflows/ci-tests.yml .. |Code coverage| image:: https://codecov.io/gh/common-workflow-language/cwltest/branch/master/graph/badge.svg :target: https://codecov.io/gh/common-workflow-language/cwltest .. |PyPI Version| image:: https://badge.fury.io/py/cwltest.svg :target: https://badge.fury.io/py/cwltest .. |PyPI Downloads Month| image:: https://pepy.tech/badge/cwltest/month :target: https://pepy.tech/project/cwltest .. |Total PyPI Downloads| image:: https://static.pepy.tech/personalized-badge/cwltest?period=total&units=international_system&left_color=black&right_color=orange&left_text=Total%20PyPI%20Downloads :target: https://pepy.tech/project/cwltest .. |Conda Version| image:: https://anaconda.org/bioconda/cwltest/badges/version.svg :target: https://anaconda.org/bioconda/cwltest .. |Conda Installs| image:: https://anaconda.org/bioconda/cwltest/badges/downloads.svg :target: https://anaconda.org/bioconda/cwltest 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.10, 3.11, 3.12, 3.13, and 3.14 .. contents:: Table of Contents :local: ******* Install ******* Installing the official package from PyPi .. code:: bash pip install cwltest Or from bioconda .. code:: bash conda install -c bioconda cwltest Or from source .. code:: bash git clone https://github.com/common-workflow-language/cwltest.git cd cwltest && pip install . *********************** Run on the command line *********************** Simple command:: cwltest --test test-descriptions.yml --tool cwl-runner ***************************************** Generate conformance badges using cwltest ***************************************** To make badges that show the results of the conformance test, you can generate JSON files for https://badgen.net by using --badgedir option To generate JSON files:: cwltest --test test-descriptions.yml --tool cwl-runner --badgedir badges ... $ cat badges/command_line_tool.json | jq . { "subject": "command_line_tool", "status": "100%", "color": "green" } Once you upload JSON file to a server, you make a badge by using a link like https://badgen.net/https/path/to/generated/json or https://flat.badgen.net/https/path/to/generated/json (for flat badges). Here is an example of markdown to add a badge:: ![test result](https://flat.badgen.net/https/path/to/generated/json?icon=commonwl) ************************* Custom file access module ************************* If your CWL implementation does not write output files to a local file system location but instead to some other remote storage system, you can provide an alternate implementation of the *StdFsAccess* object that is able to access your storage system. Step 1: Implement your own class with the same public interface of the *StdFsAccess* object in *cwltest/stdfsaccess.py* (as of this writing, the methods are *open*, *size*, *isfile* and *isdir*). These methods should expect to be called with URIs from the *location* field of the outputs of test cases. Define a function that, when called, returns a new instance of your object. Step 2: Create a Python package containing your class (or add it to an existing one). In the package metadata, add an entry point that declares the module (in this example, *my_cwl_runner.fsaccess*) containing the function (in this example, *get_fsaccess*) that *cwltest* will invoke to get an object implementing the *StdFsAccess* interface. In *setup.py* this looks like: .. code:: python setup( ... entry_points={"cwltest.fsaccess": ["fsaccess=my_cwl_runner.fsaccess:get_fsaccess"]}}, ... ) In *pyproject.toml* it looks like: .. code:: [project.entry-points.'cwltest.fsaccess'] fsaccess = 'my_cwl_runner.fsaccess:get_fsaccess' Step 3: Install your package in the same Python environemnt as the installation of *cwltest*. When invoked, *cwltest* will query Python package metadata for a package with the *cwltest.fsaccess* entry point and call it to get back a custom filesystem access object. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3949723 cwltest-2.7.20261106145043/README.rst0000664000175000017500000001110315273371155015754 0ustar00michaelmichael########################################## Common Workflow Language testing framework ########################################## |Linux Build Status| |Code coverage| PyPI: |PyPI Version| |PyPI Downloads Month| |Total PyPI Downloads| Conda: |Conda Version| |Conda Installs| .. |Linux Build Status| image:: https://github.com/common-workflow-language/cwltest/actions/workflows/ci-tests.yml/badge.svg?branch=main :target: https://github.com/common-workflow-language/cwltest/actions/workflows/ci-tests.yml .. |Code coverage| image:: https://codecov.io/gh/common-workflow-language/cwltest/branch/master/graph/badge.svg :target: https://codecov.io/gh/common-workflow-language/cwltest .. |PyPI Version| image:: https://badge.fury.io/py/cwltest.svg :target: https://badge.fury.io/py/cwltest .. |PyPI Downloads Month| image:: https://pepy.tech/badge/cwltest/month :target: https://pepy.tech/project/cwltest .. |Total PyPI Downloads| image:: https://static.pepy.tech/personalized-badge/cwltest?period=total&units=international_system&left_color=black&right_color=orange&left_text=Total%20PyPI%20Downloads :target: https://pepy.tech/project/cwltest .. |Conda Version| image:: https://anaconda.org/bioconda/cwltest/badges/version.svg :target: https://anaconda.org/bioconda/cwltest .. |Conda Installs| image:: https://anaconda.org/bioconda/cwltest/badges/downloads.svg :target: https://anaconda.org/bioconda/cwltest 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.10, 3.11, 3.12, 3.13, and 3.14 .. contents:: Table of Contents :local: ******* Install ******* Installing the official package from PyPi .. code:: bash pip install cwltest Or from bioconda .. code:: bash conda install -c bioconda cwltest Or from source .. code:: bash git clone https://github.com/common-workflow-language/cwltest.git cd cwltest && pip install . *********************** Run on the command line *********************** Simple command:: cwltest --test test-descriptions.yml --tool cwl-runner ***************************************** Generate conformance badges using cwltest ***************************************** To make badges that show the results of the conformance test, you can generate JSON files for https://badgen.net by using --badgedir option To generate JSON files:: cwltest --test test-descriptions.yml --tool cwl-runner --badgedir badges ... $ cat badges/command_line_tool.json | jq . { "subject": "command_line_tool", "status": "100%", "color": "green" } Once you upload JSON file to a server, you make a badge by using a link like https://badgen.net/https/path/to/generated/json or https://flat.badgen.net/https/path/to/generated/json (for flat badges). Here is an example of markdown to add a badge:: ![test result](https://flat.badgen.net/https/path/to/generated/json?icon=commonwl) ************************* Custom file access module ************************* If your CWL implementation does not write output files to a local file system location but instead to some other remote storage system, you can provide an alternate implementation of the *StdFsAccess* object that is able to access your storage system. Step 1: Implement your own class with the same public interface of the *StdFsAccess* object in *cwltest/stdfsaccess.py* (as of this writing, the methods are *open*, *size*, *isfile* and *isdir*). These methods should expect to be called with URIs from the *location* field of the outputs of test cases. Define a function that, when called, returns a new instance of your object. Step 2: Create a Python package containing your class (or add it to an existing one). In the package metadata, add an entry point that declares the module (in this example, *my_cwl_runner.fsaccess*) containing the function (in this example, *get_fsaccess*) that *cwltest* will invoke to get an object implementing the *StdFsAccess* interface. In *setup.py* this looks like: .. code:: python setup( ... entry_points={"cwltest.fsaccess": ["fsaccess=my_cwl_runner.fsaccess:get_fsaccess"]}}, ... ) In *pyproject.toml* it looks like: .. code:: [project.entry-points.'cwltest.fsaccess'] fsaccess = 'my_cwl_runner.fsaccess:get_fsaccess' Step 3: Install your package in the same Python environemnt as the installation of *cwltest*. When invoked, *cwltest* will query Python package metadata for a package with the *cwltest.fsaccess* entry point and call it to get back a custom filesystem access object. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793979163.6151068 cwltest-2.7.20261106145043/cwltest.egg-info/0000775000175000017500000000000015273371434017450 5ustar00michaelmichael././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793979163.5895064 cwltest-2.7.20261106145043/cwltest.egg-info/PKG-INFO0000644000175000017500000001334515273371434020551 0ustar00michaelmichaelMetadata-Version: 2.4 Name: cwltest Version: 2.7.20261106145043 Summary: Common Workflow Language testing framework Author-email: Common workflow language working group License: Apache 2.0 Project-URL: Homepage, https://github.com/common-workflow-language/cwltest Classifier: Environment :: Console Classifier: Framework :: Pytest 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.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: Programming Language :: Python :: 3.15 Classifier: Typing :: Typed Requires-Python: <3.16,>=3.10 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: schema-salad<9,>=5.0.20200220195218 Requires-Dist: junit-xml>=1.8 Provides-Extra: pytest-plugin Requires-Dist: pytest; extra == "pytest-plugin" Dynamic: license-file ########################################## Common Workflow Language testing framework ########################################## |Linux Build Status| |Code coverage| PyPI: |PyPI Version| |PyPI Downloads Month| |Total PyPI Downloads| Conda: |Conda Version| |Conda Installs| .. |Linux Build Status| image:: https://github.com/common-workflow-language/cwltest/actions/workflows/ci-tests.yml/badge.svg?branch=main :target: https://github.com/common-workflow-language/cwltest/actions/workflows/ci-tests.yml .. |Code coverage| image:: https://codecov.io/gh/common-workflow-language/cwltest/branch/master/graph/badge.svg :target: https://codecov.io/gh/common-workflow-language/cwltest .. |PyPI Version| image:: https://badge.fury.io/py/cwltest.svg :target: https://badge.fury.io/py/cwltest .. |PyPI Downloads Month| image:: https://pepy.tech/badge/cwltest/month :target: https://pepy.tech/project/cwltest .. |Total PyPI Downloads| image:: https://static.pepy.tech/personalized-badge/cwltest?period=total&units=international_system&left_color=black&right_color=orange&left_text=Total%20PyPI%20Downloads :target: https://pepy.tech/project/cwltest .. |Conda Version| image:: https://anaconda.org/bioconda/cwltest/badges/version.svg :target: https://anaconda.org/bioconda/cwltest .. |Conda Installs| image:: https://anaconda.org/bioconda/cwltest/badges/downloads.svg :target: https://anaconda.org/bioconda/cwltest 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.10, 3.11, 3.12, 3.13, and 3.14 .. contents:: Table of Contents :local: ******* Install ******* Installing the official package from PyPi .. code:: bash pip install cwltest Or from bioconda .. code:: bash conda install -c bioconda cwltest Or from source .. code:: bash git clone https://github.com/common-workflow-language/cwltest.git cd cwltest && pip install . *********************** Run on the command line *********************** Simple command:: cwltest --test test-descriptions.yml --tool cwl-runner ***************************************** Generate conformance badges using cwltest ***************************************** To make badges that show the results of the conformance test, you can generate JSON files for https://badgen.net by using --badgedir option To generate JSON files:: cwltest --test test-descriptions.yml --tool cwl-runner --badgedir badges ... $ cat badges/command_line_tool.json | jq . { "subject": "command_line_tool", "status": "100%", "color": "green" } Once you upload JSON file to a server, you make a badge by using a link like https://badgen.net/https/path/to/generated/json or https://flat.badgen.net/https/path/to/generated/json (for flat badges). Here is an example of markdown to add a badge:: ![test result](https://flat.badgen.net/https/path/to/generated/json?icon=commonwl) ************************* Custom file access module ************************* If your CWL implementation does not write output files to a local file system location but instead to some other remote storage system, you can provide an alternate implementation of the *StdFsAccess* object that is able to access your storage system. Step 1: Implement your own class with the same public interface of the *StdFsAccess* object in *cwltest/stdfsaccess.py* (as of this writing, the methods are *open*, *size*, *isfile* and *isdir*). These methods should expect to be called with URIs from the *location* field of the outputs of test cases. Define a function that, when called, returns a new instance of your object. Step 2: Create a Python package containing your class (or add it to an existing one). In the package metadata, add an entry point that declares the module (in this example, *my_cwl_runner.fsaccess*) containing the function (in this example, *get_fsaccess*) that *cwltest* will invoke to get an object implementing the *StdFsAccess* interface. In *setup.py* this looks like: .. code:: python setup( ... entry_points={"cwltest.fsaccess": ["fsaccess=my_cwl_runner.fsaccess:get_fsaccess"]}}, ... ) In *pyproject.toml* it looks like: .. code:: [project.entry-points.'cwltest.fsaccess'] fsaccess = 'my_cwl_runner.fsaccess:get_fsaccess' Step 3: Install your package in the same Python environemnt as the installation of *cwltest*. When invoked, *cwltest* will query Python package metadata for a package with the *cwltest.fsaccess* entry point and call it to get back a custom filesystem access object. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793979163.6078413 cwltest-2.7.20261106145043/cwltest.egg-info/SOURCES.txt0000664000175000017500000000512515273371434021337 0ustar00michaelmichael.coveragerc .flake8 .git-blame-ignore-revs .gitignore .readthedocs.yml CONTRIBUTING.md LICENSE MANIFEST.in Makefile README.rst mypy.ini pyproject.toml release-test.sh requirements.txt tox.ini .github/dependabot.yml .github/workflows/ci-tests.yml .github/workflows/zizmor.yml cwltest.egg-info/PKG-INFO cwltest.egg-info/SOURCES.txt cwltest.egg-info/dependency_links.txt cwltest.egg-info/entry_points.txt cwltest.egg-info/requires.txt cwltest.egg-info/scm_file_list.json cwltest.egg-info/top_level.txt cwltest.egg-info/zip-safe docs/Makefile docs/conf.py docs/index.rst docs/pytest.rst docs/requirements.txt docs/_static/favicon.ico mypy-stubs/junit_xml.pyi mypy-stubs/defusedxml/ElementTree.pyi mypy-stubs/defusedxml/__init__.pyi mypy-stubs/ruamel/__init__.pyi src/cwltest/__init__.py src/cwltest/__main__.py src/cwltest/argparser.py src/cwltest/compare.py src/cwltest/cwltest-schema-strict.yml src/cwltest/cwltest-schema.yml src/cwltest/hooks.py src/cwltest/main.py src/cwltest/plugin.py src/cwltest/py.typed src/cwltest/stdfsaccess.py src/cwltest/utils.py tests/__init__.py tests/test_argparse.py tests/test_badgedir.py tests/test_categories.py tests/test_compare.py tests/test_exclude_tags.py tests/test_integer_id.py tests/test_invalid_outputs.py tests/test_load_custom_fsaccess.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 tests/test_string_id.py tests/test_timeout.py tests/util.py tests/test-data/badgedir.yaml tests/test-data/conformance_test_v1.0.cwltest.yml tests/test-data/conformance_test_v1.2.cwltest.yaml tests/test-data/cores.txt tests/test-data/cwltool-conftest.py tests/test-data/dummy-executor.sh tests/test-data/empty.yml tests/test-data/exclude-tags.yml tests/test-data/integer-id.cwltest.yml tests/test-data/integer-id.yml tests/test-data/mock_cwl_runner.py tests/test-data/multi-lined-doc.yml tests/test-data/nothing.yml tests/test-data/optional-error.yml tests/test-data/optional-unsupported.yml tests/test-data/required-unsupported.yml tests/test-data/return-0.cwl tests/test-data/return-1.cwl tests/test-data/return-unsupported.cwl tests/test-data/short-names.yml tests/test-data/string-id.yml tests/test-data/timeout.cwl tests/test-data/timeout.yml tests/test-data/true.cwl tests/test-data/with-and-without-short-names.yml tests/test-data/v1.0/args.py tests/test-data/v1.0/cat-job.json tests/test-data/v1.0/cat-n-job.json tests/test-data/v1.0/cat1-job.json tests/test-data/v1.0/cat1-testcli.cwl tests/test-data/v1.0/cat2-job.json tests/test-data/v1.0/empty.json tests/test-data/v1.0/hello.txt tests/test-data/v1.0/null-expression2-tool.cwl././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1793979163.589836 cwltest-2.7.20261106145043/cwltest.egg-info/dependency_links.txt0000664000175000017500000000000115273371434023516 0ustar00michaelmichael ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793979163.5901716 cwltest-2.7.20261106145043/cwltest.egg-info/entry_points.txt0000664000175000017500000000011715273371434022745 0ustar00michaelmichael[console_scripts] cwltest = cwltest.main:main [pytest11] cwl = cwltest.plugin ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1793979163.590486 cwltest-2.7.20261106145043/cwltest.egg-info/requires.txt0000664000175000017500000000011315273371434022043 0ustar00michaelmichaelschema-salad<9,>=5.0.20200220195218 junit-xml>=1.8 [pytest-plugin] pytest ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1793979163.586386 cwltest-2.7.20261106145043/cwltest.egg-info/scm_file_list.json0000664000175000017500000000575515273371434023173 0ustar00michaelmichael{ "files": [ ".coveragerc", ".flake8", ".git-blame-ignore-revs", ".github/dependabot.yml", ".github/workflows/ci-tests.yml", ".github/workflows/zizmor.yml", ".gitignore", ".readthedocs.yml", "CONTRIBUTING.md", "LICENSE", "MANIFEST.in", "Makefile", "README.rst", "docs/Makefile", "docs/_static/favicon.ico", "docs/conf.py", "docs/index.rst", "docs/pytest.rst", "docs/requirements.txt", "mypy-stubs/defusedxml/ElementTree.pyi", "mypy-stubs/defusedxml/__init__.pyi", "mypy-stubs/junit_xml.pyi", "mypy-stubs/ruamel/__init__.pyi", "mypy.ini", "pyproject.toml", "release-test.sh", "requirements.txt", "src/cwltest/__init__.py", "src/cwltest/__main__.py", "src/cwltest/argparser.py", "src/cwltest/compare.py", "src/cwltest/cwltest-schema-strict.yml", "src/cwltest/cwltest-schema.yml", "src/cwltest/hooks.py", "src/cwltest/main.py", "src/cwltest/plugin.py", "src/cwltest/py.typed", "src/cwltest/stdfsaccess.py", "src/cwltest/utils.py", "tests/__init__.py", "tests/test-data/badgedir.yaml", "tests/test-data/conformance_test_v1.0.cwltest.yml", "tests/test-data/conformance_test_v1.2.cwltest.yaml", "tests/test-data/cores.txt", "tests/test-data/cwltool-conftest.py", "tests/test-data/dummy-executor.sh", "tests/test-data/empty.yml", "tests/test-data/exclude-tags.yml", "tests/test-data/integer-id.cwltest.yml", "tests/test-data/integer-id.yml", "tests/test-data/mock_cwl_runner.py", "tests/test-data/multi-lined-doc.yml", "tests/test-data/nothing.yml", "tests/test-data/optional-error.yml", "tests/test-data/optional-unsupported.yml", "tests/test-data/required-unsupported.yml", "tests/test-data/return-0.cwl", "tests/test-data/return-1.cwl", "tests/test-data/return-unsupported.cwl", "tests/test-data/short-names.yml", "tests/test-data/string-id.yml", "tests/test-data/timeout.cwl", "tests/test-data/timeout.yml", "tests/test-data/true.cwl", "tests/test-data/v1.0/args.py", "tests/test-data/v1.0/cat-job.json", "tests/test-data/v1.0/cat-n-job.json", "tests/test-data/v1.0/cat1-job.json", "tests/test-data/v1.0/cat1-testcli.cwl", "tests/test-data/v1.0/cat2-job.json", "tests/test-data/v1.0/empty.json", "tests/test-data/v1.0/hello.txt", "tests/test-data/v1.0/null-expression2-tool.cwl", "tests/test-data/with-and-without-short-names.yml", "tests/test_argparse.py", "tests/test_badgedir.py", "tests/test_categories.py", "tests/test_compare.py", "tests/test_exclude_tags.py", "tests/test_integer_id.py", "tests/test_invalid_outputs.py", "tests/test_load_custom_fsaccess.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", "tests/test_string_id.py", "tests/test_timeout.py", "tests/util.py", "tox.ini" ] } ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1793979163.590615 cwltest-2.7.20261106145043/cwltest.egg-info/top_level.txt0000664000175000017500000000001015273371434022171 0ustar00michaelmichaelcwltest ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978991.3780694 cwltest-2.7.20261106145043/cwltest.egg-info/zip-safe0000664000175000017500000000000115273371157021102 0ustar00michaelmichael ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793979163.6155782 cwltest-2.7.20261106145043/docs/0000775000175000017500000000000015273371434015221 5ustar00michaelmichael././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3949723 cwltest-2.7.20261106145043/docs/Makefile0000664000175000017500000000117215273371155016662 0ustar00michaelmichael# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793979163.6156745 cwltest-2.7.20261106145043/docs/_static/0000775000175000017500000000000015273371434016647 5ustar00michaelmichael././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3953311 cwltest-2.7.20261106145043/docs/_static/favicon.ico0000664000175000017500000003535615273371155021004 0ustar00michaelmichael00 ¨%6  ¨Þ% h†6(0` $ÿÿÿÍÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÓÿÿÿþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿüûþÿýûþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûùýÿºŸâÿ¼¡ãÿûùþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûùýÿ·žáÿvDÆÿwEÇÿ¹ âÿûúþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûúþÿ¶žáÿrDÆÿo?Åÿo?ÅÿrDÆÿȶéÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûúþÿµŸáÿnCÅÿj>Ãÿk?Ãÿi<ÃÿŒhÑÿçßöÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûúþÿ³ŸáÿjBÄÿf=Âÿg>Âÿe;Áÿ‡fÏÿåÞõÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûûþÿ±ŸàÿfAÂÿa;Áÿb<Áÿa;ÀÿƒeÎÿäÝôÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿôñûÿˆnÐÿZ7¾ÿ];¿ÿ];¿ÿ\:¿ÿ¤‘Ûÿýüþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿäßõÿ}eÍÿV7½ÿX9¾ÿX8¾ÿ_AÁÿ²¤áÿüûþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿãÞôÿxbËÿQ5¼ÿT8½ÿS7¼ÿY?¿ÿ°¤àÿüüþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿàÜóÿs`ÉÿL4»ÿN7»ÿM6»ÿU>¾ÿ°¥áÿüüþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÞÛóÿn^ÈÿG3¹ÿJ6ºÿI4ºÿQ=½ÿ®¥áÿüüþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÜÙòÿi[ÇÿC3¹ÿE5ºÿC3¹ÿL<¼ÿ¬¦àÿüüþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÙÖñÿYM¿ÿ>1¸ÿ@3¸ÿ>2¸ÿF:»ÿ´¯ãÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¿¹ÙÿG7—ÿ8-¬ÿ:1¸ÿ8/¶ÿPH¿ÿÈÅëÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¾¹ÜÿF7”ÿ1ÿ4!†ÿ3*©ÿJF¾ÿÄÂéÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¾ºÞÿG:›ÿ1 ˆÿ3 �ÿ0vÿI9�ÿÂÀæÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¾»áÿG<£ÿ1#�ÿ3#‰ÿ0ÿK8ˆÿÅ¿×ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¾¼ãÿG>ªÿ2&˜ÿ4%‘ÿ0 ‡ÿK:�ÿÅÀÚÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¾¼åÿGA±ÿ2( ÿ4(™ÿ2$‘ÿM?˜ÿÆÀÜÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿïïùÿb]Áÿ0(¨ÿ4*¡ÿ4(™ÿ2$‘ÿ”ŒÂÿþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÇÆéÿMG³ÿ1( ÿ4(™ÿ3$‘ÿG9•ÿ¼¶ÖÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÈÆçÿMD¬ÿ1%˜ÿ4%‘ÿ1 ˆÿE4Œÿ¼µÔÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÈÅåÿMB¥ÿ1#�ÿ3#‰ÿ1ÿE1…ÿ¼´ÑÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÈÄãÿM@žÿ0 ˆÿ3 �ÿ1vÿD1‚ÿ¼¸ÞÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÇÃàÿL>—ÿ0ÿ3}ÿ1&œÿFB»ÿ½»çÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÈÄÞÿM=–ÿ5)¢ÿ81·ÿ6/¶ÿJC½ÿ½ºçÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿçåöÿf\Ãÿ;0·ÿ=2¸ÿ<1¸ÿ;0·ÿŽˆÖÿüüþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿâàõÿrgËÿ@2¹ÿA3¹ÿA3¹ÿA4¹ÿ‰€ÓÿñðúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÞÛóÿoaÉÿC2¹ÿE5ºÿE4ºÿG6ºÿ�†ÖÿóòûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÙÕñÿl\ÇÿH3¹ÿJ6ºÿI5ºÿL8»ÿ—ŒØÿõôûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÔÎïÿjWÆÿL4ºÿN7»ÿM6»ÿQ:¼ÿž’Úÿ÷öüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÎÆìÿjRÅÿP5¼ÿS8¼ÿR7¼ÿX?¿ÿ§™Ýÿùøýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿüûþÿ–„×ÿS5¼ÿV9¾ÿW9¾ÿU7½ÿu]ÊÿëèøÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿëèøÿˆqÑÿY8¾ÿZ:¿ÿZ:¿ÿ\;¿ÿ—‚×ÿóñûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿëç÷ÿ‹rÑÿ]:¿ÿ_;Àÿ_;Àÿ_;Àÿ›„×ÿôñûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿìçøÿŽsÒÿb;Áÿc=Áÿc<Áÿc<ÁÿŸ‡Ùÿõòûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿìçøÿ‘sÓÿf<Âÿg>Âÿg=Âÿg>Âÿ¢ˆÚÿõóûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿìçøÿ“sÔÿj=Ãÿk?Ãÿk>Ãÿk?Äÿ¥ŠÛÿöóüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿíçøÿ–tÕÿm>Äÿo@Åÿo?Åÿn?Äÿ¯”ßÿýüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿíçøÿ˜tÕÿq?Åÿr?ÅÿzKÈÿdzèÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿíæøÿ™rÔÿ}KÉÿDzèÿþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿìä÷ÿÖÅîÿýüþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÆÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÍ( @ ÿÿÿëÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿîÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýüþÿýüþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûùýÿº âÿ»¢ãÿûúþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûúþÿ¶žâÿsCÆÿrCÆÿÁ­æÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûúþÿ´ŸáÿmCÅÿh;Âÿ}WËÿÙÍðÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿüûþÿ±žàÿgAÃÿa:ÀÿwUÉÿÖÌïÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿøöýÿ“}ÕÿZ8¾ÿZ8¾ÿ€gÍÿíêøÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿîêùÿ‰tÑÿS6¼ÿT7½ÿˆuÑÿíêøÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿìéøÿ‚qÏÿK4ºÿL5ºÿ„uÐÿíëùÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿêè÷ÿ|oÎÿF5ºÿD3¹ÿ€uÐÿìêøÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÚ×îÿRF·ÿ=1¸ÿ;/·ÿš“Úÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿóòùÿ…|¸ÿ6%ˆÿ3(£ÿa\ÆÿÜÛóÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿóòúÿ…}Àÿ3$‹ÿ0|ÿ_PšÿÜÚðÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿóòúÿ…Çÿ4(˜ÿ0 ‰ÿ`QœÿÝÚèÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿøøýÿ‡„Ïÿ4+£ÿ1%–ÿZM¢ÿÞÛëÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿùùýÿ�ŠÑÿ5,¤ÿ1%–ÿWJ ÿÙÖèÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿõõûÿŒ†Êÿ5)˜ÿ0 ‰ÿZK˜ÿØÔåÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿõõûÿŒ„Ãÿ5%Œÿ/|ÿZI’ÿØÖìÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿõõúÿŒƒ¼ÿ7%…ÿ2%šÿ\XÃÿØ×ñÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿâàòÿXM¶ÿ:/¶ÿ7-¶ÿ„}Òÿûûþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿëêøÿ�wÐÿC5ºÿ@1¸ÿlaÈÿÝÚóÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿèå÷ÿ~oÎÿG4ºÿG3¹ÿufËÿâàôÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿäßõÿ|iÍÿM5»ÿO7»ÿ~mÎÿæãöÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûûþÿ“�ÕÿR4¼ÿT7½ÿiOÅÿßÙóÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÙÒðÿvZÉÿY8¾ÿ^=Àÿ ŒÚÿöôüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÜÔñÿ{\Ëÿ`9Àÿc=Áÿ¤ŽÛÿ÷õüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÝÔòÿ€]Ìÿf;Âÿi?Ãÿ©�ÝÿøõüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÞÓòÿ…^Îÿl=Ãÿn@Äÿ±˜àÿýýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿßÔòÿ‰^ÎÿxGÇÿ­æÿþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿßÒòÿ̸êÿýûþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿèÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿë(  ÿÿÿûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýûþÿýûþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûùþÿ¹¡âÿ¿©åÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýüþÿ±žàÿrLÇÿÈ·éÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûúþÿŸŒÚÿeIÃÿÏÇìÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿõóûÿ’…ÖÿbQÄÿÌÇìÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÄ¿ßÿB4¨ÿ‘‹ÖÿþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÏÍéÿUI¢ÿzn®ÿîí÷ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ÷÷ýÿqkÀÿF:�ÿÚ×éÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÒÐêÿWK¢ÿwjªÿíìöÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÈÄáÿB5¥ÿ‰ƒÒÿüüþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿóñûÿ’†Öÿ[MÂÿ½éÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿüüþÿ›‹ÙÿY@¿ÿŽéÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿĸèÿnMÆÿ©•ÝÿùøýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿɺêÿxOÉÿ³›àÿþýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿμëÿÄ®çÿþýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþÿÿþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿû././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3953311 cwltest-2.7.20261106145043/docs/conf.py0000664000175000017500000000670215273371155016525 0ustar00michaelmichael# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys import time from datetime import datetime sys.path.insert(0, os.path.abspath("..")) # -- Project information ----------------------------------------------------- build_date = datetime.utcfromtimestamp( int(os.environ.get("SOURCE_DATE_EPOCH", time.time())) ) # -- Project information ----------------------------------------------------- project = "cwltest" copyright = ( f"2016 - {build_date.year}, Common Workflow Language project and contributors" ) author = "Common Workflow Language project and contributors" # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.autosummary", "sphinx.ext.intersphinx", "sphinx.ext.inheritance_diagram", "autoapi.extension", "sphinx_autodoc_typehints", "sphinx_rtd_theme", "sphinxcontrib.autoprogram", ] intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "schema_salad": ("https://schema-salad.readthedocs.io/en/stable/", None), #"rdflib": ("https://rdflib.readthedocs.io/en/6.2.0/", None), #"pytest": ("https://docs.pytest.org/en/7.2.x/", None), "pytest_xdist": ("https://pytest-xdist.readthedocs.io/en/latest/", None), #"ruamel.yaml": ("https://yaml.readthedocs.io/en/stable/", None), } # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] from importlib.metadata import version as metadata_version release = metadata_version("cwltest") version = ".".join(release.split(".")[:2]) autoapi_dirs = ["../src/cwltest"] autodoc_typehints = "description" autoapi_keep_files = True autoapi_ignore = ["*.pyi"] autoapi_options = [ "members", "undoc-members", "show-inheritance", "show-inheritance-diagram", "show-module-summary", "imported-members", "special-members", ] # sphinx-autodoc-typehints always_document_param_types = True # If False, do not add type info for undocumented parameters. # If True, add stub documentation for undocumented parameters to be able to add type info. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3953311 cwltest-2.7.20261106145043/docs/index.rst0000664000175000017500000000050715273371155017064 0ustar00michaelmichael.. include:: ../README.rst ******************** Command Line Options ******************** .. autoprogram:: cwltest.argparser:arg_parser() :prog: cwltest .. toctree:: :maxdepth: 2 pytest autoapi/index ****************** Indices and tables ****************** * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3953311 cwltest-2.7.20261106145043/docs/pytest.rst0000664000175000017500000001432615273371155017311 0ustar00michaelmichael************* Pytest plugin ************* ``cwltest`` can also be used as a Pytest 7.x or 8.x plugin. The CWL test filename must end with ``.cwltest.yml`` or ``.cwltest.yaml``. In this case, the simple command:: cwltest --test conformance_xxx.cwltest.yml --tool cwl-runner becomes:: pytest conformance_xxx.cwltest.yml --cwl-runner cwl-runner Command Line Options ==================== .. autoprogram:: cwltest.plugin:_doc_options() :prog: pytest Converting ``cwltest`` options to ``pytest`` options ==================================================== The table below details all the available command conversions between the two formats. .. list-table:: :widths: 40 30 30 :header-rows: 1 * - Feature - ``cwltest`` - ``pytest`` * - YAML file describing test cases - ``--test conformance_xxx.cwltest.yml`` - ``conformance_xxx.cwltest.yml`` * - CWL runner executable to use - ``--tool CWL_RUNNER`` - :option:`--cwl-runner CWL_RUNNER` * - Specifies the number of tests to run simultaneously - ``-j CORES`` - ``-n CORES`` [#f1]_ * - Automatically scale the number of tests to run simultaneously - **UNSUPPORTED** - ``-n auto`` [#f1]_ * - Only run one test at a time (good for debugging cwltest itself) - ``-j 1`` (or leave out ``-j``) - ``-n 0 -s`` [#f1]_ * - Time of execution in seconds after which the test will be skipped - ``--timeout TIMEOUT`` - ``--timeout TIMEOUT`` [#f3]_ * - List tests then exit - ``-l`` - ``--collect-only`` * - Run specific tests using their short names - ``-s TEST_NAME[,TEST_NAME…]`` - :option:`--cwl-include TEST_NAME[,TEST_NAME…]` * - Exclude specific tests by short names - ``-S TEST_NAME[,TEST_NAME…]`` - :option:`--cwl-exclude TEST_NAME[,TEST_NAME…]` * - Tags to be tested - ``--tags TAG[,TAG…]`` - :option:`--cwl-tags TAG[,TAG…]` * - Tags not to be tested - ``--exclude-tags TAG[,TAG…]`` - :option:`--cwl-exclude-tags TAG[,TAG…]` * - Path to JUnit xml file - ``--junit-xml PATH`` - ``--junit-xml=PATH`` [#f4]_ * - More verbose output during test run - ``--verbose`` - ``-v[vv]`` * - Additional argument given in test cases and required prefix for tool runner - ``--test-arg ARG_NAME==ARG_PREFIX`` - :option:`--cwl-test-arg ARG_NAME==ARG_PREFIX` * - Arguments to pass first to tool runner - ``cwltest -- ARG [ARG …]`` - :option:`--cwl-args "ARG [ARG …]"` * - Only test CommandLineTools - ``--only-tools`` - **UNSUPPORTED** * - Show all tags - ``--show-tags`` - **UNSUPPORTED** * - Store more verbose output to JUnit xml file - ``--junit-verbose`` - :option:`--cwl-runner-verbose` [#f4]_ * - Specify classname for the Test Suite - ``--classname CLASS_NAME`` - **UNSUPPORTED** .. [#f1] Requires `pytest-xdist `_. See :ref:`pytest_xdist:parallelization`. .. [#f2] ``-s`` is a shortcut for ``--capture=no``, also helps with debugging ``cwltest`` or the cwltest plugin to ``pytest``. .. [#f3] Requires `pytest-timeout `_. Note: even if ``pytest-timeout`` is installed, there is no default timeout. This is different than ``cwltest``'s default timeout of 10 minutes. Differences in the XML output ============================= ``cwltest --junit-xml`` output * top-level ```` element has the elapsed time, and counts (errors, failures, skipped, and total) * singular ```` sub-element the same attributes as the top-level ```` plus ``name`` which is the basename of the YAML test file * each ```` element has the follow attributes * ``name``: the doc string * ``class``: the tags * ``file``: the test ID * ``url``: like "cwltest:conformance_tests#1" (contains the basename of the YAML test file) * ``time``: the elapsed time * ```` elements always contain the following sub-elements, regardless of outcome * ````: the output object * ````: stderr (docker pull, other warnings, and errors) * ```` elements for failed test cases do not have a ```` sub-element ``pytest`` with ``cwltest`` plugin XML output * top-level ```` element has no attributes * singular ```` sub-element has the same attributes as the ``cwltest`` XML version along with these additional attributes * ``name``: default is ``pytest`` (can be customized with the pytest INI option ``junit_suite_name``) * ``timestamp="2023-01-08T11:39:07.425159"`` * ``hostname``: the hostname of the machine where the tests ran * inside the ```` is a ``..`` element with two ```` elements. But this `does not work with pytest-xdist `_. * ``runner``: the name of the CWL runner * ``runner_extra_args``: the value of `--cwl-args`` * each ```` element has the following attributes * ``classname``: always the name of the YAML file (``conformance_test_v1.2.cwltest.yaml``) * ``name``: the test ID * ``time``: the elapsed time * ```` elements for failed test cases **do** have a ```` sub-element with a ``message`` attribute containing the :py:meth:`cwltest.plugin.CWLItem.repr_failure` output. This text is repeated as the content of the ```` element. The presensce of ```` and ```` sub-elements varies. [#f4]_ .. [#f4] Depending on the value of the pytest INI option ``junit_logging``, then ```` and ```` sub-elements will be generated. However the default value for ``junit_logging`` is ``no``, so to get either of these pick one from `the full list `_. You can set ``junit_logging`` in `a configuration file `_ or on the command line: ``pytest -o junit_logging=out-err``. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3953311 cwltest-2.7.20261106145043/docs/requirements.txt0000664000175000017500000000015015273371155020501 0ustar00michaelmichaelsphinx >= 2.2 sphinx-rtd-theme==3.1.0 sphinx-autoapi sphinx-autodoc-typehints sphinxcontrib-autoprogram ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793979163.6157694 cwltest-2.7.20261106145043/mypy-stubs/0000775000175000017500000000000015273371434016425 5ustar00michaelmichael././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1793979163.615978 cwltest-2.7.20261106145043/mypy-stubs/defusedxml/0000775000175000017500000000000015273371434020565 5ustar00michaelmichael././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3955505 cwltest-2.7.20261106145043/mypy-stubs/defusedxml/ElementTree.pyi0000664000175000017500000000026415273371155023523 0ustar00michaelmichaelimport xml.etree.ElementTree from pathlib import Path def parse( source: Path = ..., parser: xml.etree.ElementTree.XMLParser = ... ) -> xml.etree.ElementTree.ElementTree: ... ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3955505 cwltest-2.7.20261106145043/mypy-stubs/defusedxml/__init__.pyi0000664000175000017500000000000015273371155023035 0ustar00michaelmichael././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1793978989.395586 cwltest-2.7.20261106145043/mypy-stubs/junit_xml.pyi0000664000175000017500000000473115273371155021166 0ustar00michaelmichael# Stubs for junit_xml (Python 3.5) # # NOTE: This dynamically typed stub was automatically generated by stubgen. from typing import IO, Any, List, Optional unichr = chr class TestSuite: name: Any = ... test_cases: Any = ... timestamp: Any = ... hostname: Any = ... id: Any = ... package: Any = ... file: Any = ... log: Any = ... url: Any = ... stdout: Any = ... stderr: Any = ... properties: Any = ... def __init__( self, name: str, test_cases: Optional[Any] = ..., hostname: Optional[Any] = ..., id: Optional[Any] = ..., package: Optional[Any] = ..., timestamp: Optional[Any] = ..., properties: Optional[Any] = ..., file: Optional[Any] = ..., log: Optional[Any] = ..., url: Optional[Any] = ..., stdout: Optional[Any] = ..., stderr: Optional[Any] = ..., ) -> None: ... class TestCase: name: Any = ... assertions: Any = ... elapsed_sec: Any = ... timestamp: Any = ... classname: Any = ... status: Any = ... category: Any = ... file: Any = ... line: Any = ... log: Any = ... url: Any = ... stdout: Any = ... stderr: Any = ... is_enabled: bool = ... error_message: Any = ... error_output: Any = ... error_type: Any = ... failure_message: Any = ... failure_output: Any = ... failure_type: Any = ... skipped_message: Any = ... skipped_output: Any = ... def __init__( self, name: str, classname: Optional[Any] = ..., elapsed_sec: Optional[Any] = ..., stdout: Optional[Any] = ..., stderr: Optional[Any] = ..., assertions: Optional[Any] = ..., timestamp: Optional[Any] = ..., status: Optional[Any] = ..., category: Optional[Any] = ..., file: Optional[Any] = ..., line: Optional[Any] = ..., log: Optional[Any] = ..., group: Optional[Any] = ..., url: Optional[Any] = ..., ) -> None: ... def add_failure_info( self, message: Optional[Any] = ..., output: Optional[Any] = ..., failure_type: Optional[Any] = ..., ) -> None: ... def add_skipped_info( self, message: Optional[Any] = ..., output: Optional[Any] = ... ) -> None: ... def to_xml_report_file( file_descriptor: IO[Any], test_suites: List[TestSuite], prettyprint: bool = True, encoding: Optional[str] = None, ) -> None: ... ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793979163.6160762 cwltest-2.7.20261106145043/mypy-stubs/ruamel/0000775000175000017500000000000015273371434017712 5ustar00michaelmichael././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1793978989.395586 cwltest-2.7.20261106145043/mypy-stubs/ruamel/__init__.pyi0000664000175000017500000000000015273371155022162 0ustar00michaelmichael././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1793978989.395586 cwltest-2.7.20261106145043/mypy.ini0000664000175000017500000000025615273371155015773 0ustar00michaelmichael[mypy] strict = True show_error_context = true show_column_numbers = true show_error_codes = true pretty = true warn_unreachable = True [mypy-ruamel.*] ignore_errors = True ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1793978989.395586 cwltest-2.7.20261106145043/pyproject.toml0000664000175000017500000000444315273371155017212 0ustar00michaelmichael[build-system] requires = [ "setuptools>=61.2", "setuptools_scm[simple]>=8.0.4,<11", ] build-backend = "setuptools.build_meta" [project] name = "cwltest" authors = [{name = "Common workflow language working group", email = "common-workflow-language@googlegroups.com"}] license = {text = "Apache 2.0"} description = "Common Workflow Language testing framework" classifiers = [ "Environment :: Console", "Framework :: Pytest", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX", "Operating System :: MacOS :: MacOS X", "Development Status :: 5 - Production/Stable", "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", "Programming Language :: Python :: 3.15", "Typing :: Typed", ] requires-python = ">=3.10,<3.16" dependencies = [ "schema-salad >= 5.0.20200220195218, < 9", "junit-xml >= 1.8", ] dynamic = ["version"] [project.readme] file = "README.rst" content-type = "text/x-rst" [project.urls] Homepage = "https://github.com/common-workflow-language/cwltest" [project.entry-points.pytest11] cwl = "cwltest.plugin" [project.optional-dependencies] pytest-plugin = ["pytest"] [project.scripts] cwltest = "cwltest.main:main" [dependency-groups] test = [ "cwlref-runner", "cwltool", "pytest-cov", "pytest-xdist", "defusedxml", ] lint = [ "flake8-bugbear", "black~=26.1", ] mypy = [ "mypy==2.4.1", "types-setuptools", "types-PyYAML", "cwltool", ] dev = [ {include-group = "test"}, {include-group = "lint"}, {include-group = "mypy"}, "black ~= 26.10", "pylint", "pep257", "pydocstyle", "flake8", "tox >4, < 5", "isort", "wheel", "autoflake", "flake8-bugbear", "pyupgrade", "bandit", "auto-walrus", "build", ] [tool.aliases] test = "pytest" [tool.setuptools] package-dir = {"cwltest" = "src/cwltest", "cwltest.tests" = "tests"} packages = ["cwltest", "cwltest.tests"] zip-safe = true include-package-data = true [tool.setuptools.package-data] cwltest = ["py.typed"] tests = ["test-data/*"] [tool.isort] multi_line_output = "3" include_trailing_comma = "True" force_grid_wrap = "0" use_parentheses = "True" line_length = "88" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1793978989.395586 cwltest-2.7.20261106145043/release-test.sh0000775000175000017500000000673715273371155017242 0ustar00michaelmichael#!/bin/bash set -e set -x export LC_ALL=C package=cwltest module=cwltest extras="" if [ "$GITHUB_ACTIONS" = "true" ]; then # We are running as a GH Action repo=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git HEAD=${GITHUB_REF} else repo=https://github.com/common-workflow-language/cwltest.git HEAD=$(git rev-parse HEAD) fi run_tests="python -m pytest -p pytester --pyargs ${module}" pipver=25.1 # minimum required version of pip for PEP 735 dependency groups setuptoolsver=67.6.1 # required for Python 3.12 DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" rm -Rf testenv? || /bin/true if [ "${RELEASE_SKIP}" != "head" ] then python3 -m venv testenv1 # First we test the head # shellcheck source=/dev/null source testenv1/bin/activate rm -Rf testenv1/local rm -f testenv1/lib/python-wheels/setuptools* \ && pip install --force-reinstall -U pip==${pipver} \ && pip install setuptools==${setuptoolsver} wheel pip install --group test ".${extras}" pip show cwltest | grep Version | grep -v -q 0.0.0 make test pip uninstall -y ${package} || true; pip uninstall -y ${package} || true; make install pip show cwltest | grep Version | grep -v -q 0.0.0 ${run_tests} fi python3 -m venv testenv2 python3 -m venv testenv3 python3 -m venv testenv4 python3 -m venv testenv5 rm -Rf testenv[2345]/local # Secondly we test via pip pushd testenv2 # shellcheck source=/dev/null source bin/activate rm -f lib/python-wheels/setuptools* \ && pip install --force-reinstall -U pip==${pipver} \ && pip install setuptools==${setuptoolsver} wheel # The following can fail if you haven't pushed your commits to ${repo} pip install -e "git+${repo}@${HEAD}#egg=${package}${extras}" pip show cwltest | grep Version | grep -v -q 0.0.0 pushd src/${package} pip install --group test build pip show cwltest | grep Version | grep -v -q 0.0.0 make dist make test cp dist/${package}*tar.gz ../../../testenv3/ cp dist/${module}*whl ../../../testenv4/ pip uninstall -y ${package} || true; pip uninstall -y ${package} || true; make install pip show cwltest | grep Version | grep -v -q 0.0.0 popd # ../.. no subdir named ${proj} here, safe for py.testing the installed module # shellcheck disable=SC2086 ${run_tests} popd # Is the source distribution in testenv2 complete enough to build # another functional distribution? pushd testenv3/ # shellcheck source=/dev/null source bin/activate rm -f lib/python-wheels/setuptools* \ && pip install --force-reinstall -U pip==${pipver} \ && pip install setuptools==${setuptoolsver} wheel package_tar=$(find . -name "${package}*tar.gz") pip install build pip install "${package_tar}${extras}" pip show cwltest | grep Version | grep -v -q 0.0.0 mkdir out tar --extract --directory=out -z -f ${package}*.tar.gz pushd out/${package}* make dist pip install --group test make test pip install --group mypy pip show cwltest | grep Version | grep -v -q 0.0.0 make mypy pip uninstall -y ${package} || true; pip uninstall -y ${package} || true; make install pip show cwltest | grep Version | grep -v -q 0.0.0 ${run_tests} popd popd # Is the wheel in testenv2 installable and will it pass the tests pushd testenv4/ # shellcheck source=/dev/null source bin/activate rm -f lib/python-wheels/setuptools* \ && pip install --force-reinstall -U pip==${pipver} \ && pip install setuptools==${setuptoolsver} wheel pip install "$(ls ${module}*.whl)${extras}" pip show cwltest | grep Version | grep -v -q 0.0.0 pip install --group "${DIR}/pyproject.toml:test" ${run_tests} popd ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1793978989.395586 cwltest-2.7.20261106145043/requirements.txt0000664000175000017500000000007115273371155017553 0ustar00michaelmichaelschema-salad >= 5.0.20200220195218, < 9 junit-xml >= 1.8 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793979163.6246634 cwltest-2.7.20261106145043/setup.cfg0000664000175000017500000000004615273371434016112 0ustar00michaelmichael[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793979163.6108282 cwltest-2.7.20261106145043/src/0000775000175000017500000000000015273371434015060 5ustar00michaelmichael././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1793979163.617179 cwltest-2.7.20261106145043/src/cwltest/0000775000175000017500000000000015273371434016545 5ustar00michaelmichael././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1793978989.395771 cwltest-2.7.20261106145043/src/cwltest/__init__.py0000775000175000017500000000051615273371155020663 0ustar00michaelmichael"""Run CWL descriptions with a cwl-runner, and look for expected output.""" import logging import threading UNSUPPORTED_FEATURE = 33 DEFAULT_TIMEOUT = 600 # 10 minutes REQUIRED = "required" logger = logging.getLogger("cwltest") logger.addHandler(logging.StreamHandler()) logger.setLevel(logging.INFO) templock = threading.Lock() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3958092 cwltest-2.7.20261106145043/src/cwltest/__main__.py0000664000175000017500000000012215273371155020632 0ustar00michaelmichael"""Default entrypoint for the cwltest module.""" from . import main main.main() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3958092 cwltest-2.7.20261106145043/src/cwltest/argparser.py0000664000175000017500000000770015273371155021111 0ustar00michaelmichael"""Command line argument parsing for cwltest.""" import argparse import sys from importlib.metadata import PackageNotFoundError, version from cwltest import DEFAULT_TIMEOUT def arg_parser() -> argparse.ArgumentParser: """Generate a command Line argument parser for cwltest.""" parser = argparse.ArgumentParser( description="Common Workflow Language testing framework" ) parser.add_argument( "--test", type=str, help="YAML file describing test cases", required=True ) parser.add_argument( "--basedir", type=str, help="Basedir to use for tests", default="." ) parser.add_argument( "--baseuri", type=str, help="Base URI to use links in the report", default=None ) parser.add_argument("-l", action="store_true", help="List tests then exit") parser.add_argument( "-n", type=str, default=None, help="Run specific tests, format is 1,3-6,9" ) parser.add_argument( "-s", type=str, default=None, help="Run specific tests using their short names separated by comma", ) parser.add_argument( "-N", type=str, default=None, help="Exclude specific tests by number, format is 1,3-6,9", ) parser.add_argument( "-S", type=str, default=None, help="Exclude specific tests by short names separated by comma", ) parser.add_argument( "--tool", type=str, default="cwl-runner", help="CWL runner executable to use (default 'cwl-runner'", ) parser.add_argument( "--only-tools", action="store_true", help="Only test CommandLineTools" ) parser.add_argument("--tags", type=str, default=None, help="Tags to be tested") parser.add_argument( "--exclude-tags", type=str, default=None, help="Tags not to be tested" ) parser.add_argument("--show-tags", action="store_true", help="Show all Tags.") parser.add_argument( "--junit-xml", type=str, default=None, help="Path to JUnit xml file" ) parser.add_argument( "--junit-verbose", action="store_true", help="Store more verbose output to JUnit XML file by not passing " "'--quiet' to the CWL runner.", ) parser.add_argument( "--test-arg", type=str, help="Additional argument " "given in test cases and required prefix for tool runner.", default=None, metavar="cache==--cache-dir", action="append", dest="testargs", ) parser.add_argument( "args", help="arguments to pass first to tool runner", nargs=argparse.REMAINDER ) parser.add_argument( "-j", type=int, default=1, help="Specifies the number of tests to run simultaneously " "(defaults to one).", ) parser.add_argument( "--verbose", action="store_true", help="More verbose output during test run." ) parser.add_argument( "--classname", type=str, default="", help="Specify classname for the Test Suite.", ) parser.add_argument( "--timeout", type=int, default=DEFAULT_TIMEOUT, help="Time of execution in seconds after which the test will be " "skipped. Defaults to {} seconds ({} minutes).".format( DEFAULT_TIMEOUT, DEFAULT_TIMEOUT / 60 ), ) parser.add_argument( "--badgedir", type=str, help="Create JSON badges, one for each tag (plus a computed 'all' tag) " " and store them in this directory.", ) parser.add_argument( "--outdir", type=str, default=None, help="Specify the output directory to use for tests. Defaults to " "the system temporary directory.", ) try: ver = version("cwltest") except PackageNotFoundError: ver = "unknown version" parser.add_argument("--version", action="version", version=f"{sys.argv[0]} {ver}") return parser ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3958092 cwltest-2.7.20261106145043/src/cwltest/compare.py0000664000175000017500000002125515273371155020552 0ustar00michaelmichael"""Compare utilities for CWL objects.""" import hashlib import json from collections.abc import Callable from typing import Any import cwltest.stdfsaccess fs_access = cwltest.stdfsaccess.StdFsAccess("") class CompareFail(Exception): """Compared CWL objects are not equal.""" @classmethod def format( cls, expected: Any, actual: Any, cause: Any | None = None ) -> "CompareFail": """Load the difference details into the error message.""" message = "expected: {}\ngot: {}".format( json.dumps(expected, indent=4, sort_keys=True), json.dumps(actual, indent=4, sort_keys=True), ) if cause: message += "\ncaused by: %s" % cause return cls(message) def _check_keys( keys: set[str], expected: dict[str, Any], actual: dict[str, Any], skip_details: bool ) -> None: for k in keys: try: compare(expected.get(k), actual.get(k), skip_details) except CompareFail as e: raise CompareFail.format( expected, actual, f"field {k!r} failed comparison: {str(e)}" ) from e 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: raise CompareFail.format( expected, actual, json.dumps( "Output file contents do not match: actual '%s' is not equal to expected '%s'" % (actual_contents, expected_contents) ), ) def _compare_dict( expected: dict[str, Any], actual: dict[str, Any], skip_details: bool ) -> None: for c in expected: try: compare(expected[c], actual.get(c), skip_details) except CompareFail as e: raise CompareFail.format( expected, actual, f"failed comparison for key {c!r}: {e}" ) from e extra_keys = set(actual.keys()).difference(list(expected.keys())) for k in extra_keys: if actual[k] is not None: raise CompareFail.format(expected, actual, "unexpected key '%s'" % k) def _compare_directory( expected: dict[str, Any], actual: dict[str, Any], skip_details: bool ) -> None: if actual.get("class") != "Directory": raise CompareFail.format( expected, actual, "expected object with a class 'Directory'" ) if "listing" not in actual: raise CompareFail.format( expected, actual, "'listing' is mandatory field in Directory object" ) for i in expected["listing"]: found = False for j in actual["listing"]: try: compare(i, j, skip_details) found = True break except CompareFail: pass if not found: raise CompareFail.format( expected, actual, "%s not found" % json.dumps(i, indent=4, sort_keys=True), ) _compare_file(expected, actual, skip_details) def _compare_file( expected: dict[str, Any], actual: dict[str, Any], skip_details: bool ) -> None: _compare_location(expected, actual, skip_details) if "contents" in expected: _compare_contents(expected, actual) if actual.get("class") == "File" and not skip_details: _compare_checksum(expected, actual) _compare_size(expected, actual) other_keys = set(expected.keys()) - { "path", "location", "listing", "contents", "checksum", "size", } _check_keys(other_keys, expected, actual, skip_details) def _compare_location( expected: dict[str, Any], actual: dict[str, Any], skip_details: bool ) -> None: if "path" in expected: expected_comp = "path" if "path" not in actual: actual["path"] = actual["location"] elif "location" in expected: expected_comp = "location" else: return if "path" in actual: actual_comp = "path" else: actual_comp = "location" if actual.get("class") == "Directory": actual[actual_comp] = actual[actual_comp].rstrip("/") exist_fun: Callable[[str], bool] = fs_access.isdir else: exist_fun = fs_access.isfile if not exist_fun(actual[actual_comp]) and not skip_details: raise CompareFail.format( expected, actual, f"{actual[actual_comp]} does not exist", ) if expected[expected_comp] != "Any" and ( not ( actual[actual_comp].endswith("/" + expected[expected_comp]) or ( "/" not in actual[actual_comp] and expected[expected_comp] == actual[actual_comp] ) ) ): raise CompareFail.format( expected, actual, f"{actual[actual_comp]} does not end with {expected[expected_comp]}", ) def _compare_checksum(expected: dict[str, Any], actual: dict[str, Any]) -> None: if "path" in actual: path = actual["path"] else: path = actual["location"] checksum = hashlib.sha1() # nosec with fs_access.open(path, "rb") as f: contents = f.read(1024 * 1024) while contents != b"": checksum.update(contents) contents = f.read(1024 * 1024) actual_checksum_on_disk = f"sha1${checksum.hexdigest()}" if "checksum" in actual: actual_checksum_declared = actual["checksum"] if actual_checksum_on_disk != actual_checksum_declared: raise CompareFail.format( expected, actual, "Output file checksums do not match: actual " f"{actual_checksum_on_disk!r} on disk is not equal to actual " f"{actual_checksum_declared!r} in the output object", ) if "checksum" in expected: expected_checksum = expected["checksum"] if expected_checksum != actual_checksum_on_disk: raise CompareFail.format( expected, actual, "Output file checksums do not match: actual " f"{actual_checksum_on_disk!r} is not equal to expected {expected_checksum!r}", ) def _compare_size(expected: dict[str, Any], actual: dict[str, Any]) -> None: path = actual.get("path", actual["location"]) actual_size_on_disk = fs_access.size(path) if "size" in actual: actual_size_declared = actual["size"] if actual_size_on_disk != actual_size_declared: raise CompareFail.format( expected, actual, "Output file sizes do not match: actual " f"{actual_size_on_disk!r} on disk is not equal to actual " f"{actual_size_declared!r}' in the output object", ) if "size" in expected: expected_size = expected["size"] if expected_size != actual_size_on_disk: raise CompareFail.format( expected, actual, "Output file sizes do not match: actual " f"{actual_size_on_disk!r} is not equal to expected {expected_size!r}", ) def compare(expected: Any, actual: Any, skip_details: bool = False) -> None: """Compare two CWL objects.""" if expected == "Any": return if expected is not None and actual is None: raise CompareFail.format(expected, actual) try: match expected: case dict(): if not isinstance(actual, dict): raise CompareFail.format(expected, actual) match expected.get("class"): case "File": _compare_file(expected, actual, skip_details) case "Directory": _compare_directory(expected, actual, skip_details) case _: _compare_dict(expected, actual, skip_details) case list(): if not isinstance(actual, list): raise CompareFail.format(expected, actual) if len(expected) != len(actual): raise CompareFail.format(expected, actual, "lengths don't match") for c in range(0, len(expected)): try: compare(expected[c], actual[c], skip_details) except CompareFail as e: raise CompareFail.format(expected, actual, e) from e case _: if expected != actual: raise CompareFail.format(expected, actual) except Exception as e: raise CompareFail(str(e)) from e ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3958092 cwltest-2.7.20261106145043/src/cwltest/cwltest-schema-strict.yml0000664000175000017500000000104415273371155023520 0ustar00michaelmichael$base: "https://w3id.org/cwl/cwltest#" $graph: - name: TestCase type: record documentRoot: true fields: id: type: string jsonldPredicate: _type: "@id" identity: true short_name: string? doc: string? tags: string[] tool: type: string jsonldPredicate: _type: "@id" job: type: string? jsonldPredicate: _type: "@id" should_fail: type: boolean? default: false output: type: Any? ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3958092 cwltest-2.7.20261106145043/src/cwltest/cwltest-schema.yml0000664000175000017500000000111115273371155022205 0ustar00michaelmichael$base: "https://w3id.org/cwl/cwltest#" $graph: - name: TestCase type: record documentRoot: true fields: id: type: ["null", int, string] jsonldPredicate: _type: "@id" identity: true label: string? short_name: string? doc: string? tags: string[]? tool: type: string jsonldPredicate: _type: "@id" job: type: string? jsonldPredicate: _type: "@id" should_fail: type: boolean? default: false output: type: Any? ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3958092 cwltest-2.7.20261106145043/src/cwltest/hooks.py0000664000175000017500000000130515273371155020241 0ustar00michaelmichael"""Hooks for pytest-cwl users.""" from typing import Any from cwltest import utils def pytest_cwl_execute_test( # type: ignore[empty-body] config: utils.CWLTestConfig, processfile: str, jobfile: str | None ) -> tuple[int, dict[str, Any] | None]: """ Execute CWL test using a Python function instead of a command line runner. The return value is a tuple. - status code - 0 = success - :py:attr:`cwltest.UNSUPPORTED_FEATURE` for an unsupported feature - and any other number for failure - CWL output object using plain Python objects. :param processfile: a path to a CWL document :param jobfile: an optionl path to JSON/YAML input object """ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3958092 cwltest-2.7.20261106145043/src/cwltest/main.py0000664000175000017500000001757415273371155020061 0ustar00michaelmichael#!/usr/bin/env python3 """Entry point for cwltest.""" import argparse import os import sys from collections import Counter, defaultdict from concurrent.futures import ThreadPoolExecutor from typing import 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, CWLTestReport, TestResult, load_optional_fsaccess_plugin, ) if sys.stderr.isatty(): PREFIX = "\r" SUFFIX = "" else: PREFIX = "" SUFFIX = "\n" def _run_test( args: argparse.Namespace, test: dict[str, str], test_number: int, total_tests: int, ) -> TestResult: if test.get("short_name"): sys.stderr.write( "%sTest [%i/%i] %s: %s%s\n" % ( PREFIX, test_number, total_tests, test.get("short_name"), test.get("doc", "").replace("\n", " ").strip(), SUFFIX, ) ) else: sys.stderr.write( "%sTest [%i/%i] %s%s\n" % ( PREFIX, test_number, total_tests, test.get("doc", "").replace("\n", " ").strip(), SUFFIX, ) ) sys.stderr.flush() config = CWLTestConfig( basedir=args.basedir, test_baseuri=args.baseuri, test_basedir=args.test_basedir, classname=args.classname, entry=args.test, entry_line=test["line"], tool=args.tool, args=args.args, testargs=args.testargs, timeout=args.timeout, verbose=args.verbose, runner_quiet=not args.junit_verbose, outdir=args.outdir, ) return utils.run_test_plain(config, test, test_number) def _expand_number_range(nr: str) -> list[int]: result: list[int] = [] for s in nr.split(","): sp = s.split("-") if len(sp) == 2: result.extend(range(int(sp[0]) - 1, int(sp[1]))) else: result.append(int(s) - 1) return result def main() -> int: """Run the main logic loop.""" args = arg_parser().parse_args(sys.argv[1:]) if "--" in args.args: args.args.remove("--") # Remove test arguments with wrong syntax if args.testargs is not None: args.testargs = [ testarg for testarg in args.testargs if testarg.count("==") == 1 ] if not args.test: arg_parser().print_help() return 1 args.test_basedir = os.path.dirname(utils.absuri(args.test)) + "/" if args.baseuri is None: args.baseuri = "file://" + args.test_basedir if not args.baseuri.endswith("/"): args.baseuri = args.baseuri + "/" try: tests, metadata = utils.load_and_validate_tests(args.test) except ValidationException: logger.error(f"Failed validating test file: {args.test}", exc_info=args.verbose) return 1 failures = 0 unsupported = 0 suite_name, _ = os.path.splitext(os.path.basename(args.test)) report: junit_xml.TestSuite | None = junit_xml.TestSuite(suite_name, []) load_optional_fsaccess_plugin() ntotal: dict[str, int] = Counter() npassed: dict[str, list[CWLTestReport]] = defaultdict(list) if args.only_tools: alltests = tests tests = [] for t in alltests: loader = schema_salad.ref_resolver.Loader({"id": "@id"}) cwl = loader.resolve_ref(t["tool"])[0] if isinstance(cwl, dict): if cwl["class"] == "CommandLineTool": tests.append(t) else: raise Exception("Unexpected code path.") if args.tags: tags = set(args.tags.split(",")) tests = [t for t in tests if tags.intersection(t.get("tags", []))] if args.exclude_tags: tags = set(args.exclude_tags.split(",")) tests = [t for t in tests if not tags.intersection(t.get("tags", []))] for t in tests: if t.get("label"): logger.warning("The `label` field is deprecated. Use `id` field instead.") t["short_name"] = t["label"] # noqa: B909 elif t.get("id"): if isinstance(t.get("id"), str): t["short_name"] = utils.shortname(t["id"]) else: logger.warning( "The `id` field with integer is deprecated. Use string identifier instead." ) else: logger.warning("The `id` field is missing.") if args.show_tags: alltags: set[str] = set() for t in tests: ts = t.get("tags", []) alltags |= set(ts) for tag in sorted(alltags): print(tag) return 0 if args.l: for i, t in enumerate(tests): if t.get("short_name"): print( "[%i] %s: %s" % ( i + 1, t["short_name"], t.get("doc", "").replace("\n", " ").strip(), ) ) else: print("[%i] %s" % (i + 1, t.get("doc", "").replace("\n", " ").strip())) return 0 if args.n is not None or args.s is not None: ntest = [] if args.n is not None: ntest = _expand_number_range(args.n) if args.s is not None: for s in args.s.split(","): test_number = utils.get_test_number_by_key(tests, "short_name", s) if test_number: ntest.append(test_number) else: logger.error('Test with short name "%s" not found ', s) return 1 else: ntest = list(range(0, len(tests))) exclude_n = [] if args.N is not None: exclude_n = _expand_number_range(args.N) if args.S is not None: for s in args.S.split(","): test_number = utils.get_test_number_by_key(tests, "short_name", s) if test_number: exclude_n.append(test_number) else: logger.error('Test with short name "%s" not found ', s) return 1 ntest = list(filter(lambda x: x not in exclude_n, ntest)) total = 0 with ThreadPoolExecutor(max_workers=args.j) as executor: jobs = [ executor.submit( _run_test, args, tests[i], i + 1, len(tests), ) for i in ntest ] try: ( total, passed, failures, unsupported, ntotal, npassed, nfailures, nunsupported, report, ) = utils.parse_results( (job.result() for job in jobs), tests, suite_name, report ) except KeyboardInterrupt: for job in jobs: job.cancel() logger.error("Tests interrupted") if args.junit_xml: with open(args.junit_xml, "w") as xml: junit_xml.to_xml_report_file(xml, [cast(junit_xml.TestSuite, report)]) if args.badgedir: utils.generate_badges(args.badgedir, ntotal, npassed, nfailures, nunsupported) if failures == 0 and unsupported == 0: logger.info("All tests passed") return 0 if failures == 0 and unsupported > 0: logger.warning( "%i tests passed, %i unsupported features", total - unsupported, unsupported ) return 0 logger.warning( "%i tests passed, %i failures, %i unsupported features", total - (failures + unsupported), failures, unsupported, ) return 1 if __name__ == "__main__": sys.exit(main()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3958092 cwltest-2.7.20261106145043/src/cwltest/plugin.py0000664000175000017500000003545115273371155020425 0ustar00michaelmichael"""Discovers CWL test files and converts them to pytest.Items.""" 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, Optional, Protocol, Union, cast from urllib.parse import urljoin import pytest from cwltest import REQUIRED, UNSUPPORTED_FEATURE, logger, utils from cwltest.compare import CompareFail, compare if TYPE_CHECKING: from _pytest._code.code import TracebackStyle from _pytest.nodes import Node from pluggy import HookCaller class TestRunner(Protocol): """Protocol to type-check test runner functions via the pluggy hook.""" def __call__( self, config: utils.CWLTestConfig, processfile: str, jobfile: str | None ) -> list[dict[str, Any] | None]: """Type signature for pytest_cwl_execute_test hook results.""" ... def _get_comma_separated_option(config: pytest.Config, name: str) -> list[str]: options = config.getoption(name) if options is None: return [] elif "," in options: return [opt.strip() for opt in options.split(",")] else: return [options.strip()] def _run_test_hook_or_plain( test: dict[str, str], config: utils.CWLTestConfig, hook: "HookCaller", ) -> utils.TestResult: """Run tests using a provided pytest_cwl_execute_test hook or the --cwl-runner.""" processfile, jobfile = utils.prepare_test_paths(test, config.basedir) start_time = time.time() reltool = os.path.relpath(test["tool"], start=config.test_basedir) tooluri = urljoin(config.test_baseuri, reltool) if test.get("job", None): reljob = os.path.relpath(test["job"], start=config.test_basedir) joburi = urljoin(config.test_baseuri, reljob) else: joburi = None outerr = "" 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]) duration = time.time() - start_time outstr = json.dumps(out) if out is not None else "{}" if returncode == UNSUPPORTED_FEATURE: if REQUIRED not in test.get("tags", ["required"]): return utils.TestResult( UNSUPPORTED_FEATURE, outstr, "", duration, config.classname, config.entry, tooluri, joburi, ) elif returncode != 0: if not bool(test.get("should_fail", False)): logger.warning("Test failed unexpectedly: %s %s", processfile, jobfile) logger.warning(test.get("doc")) message = "Returned non-zero but it should be zero" return utils.TestResult( 1, outstr, outerr, duration, config.classname, config.entry, tooluri, joburi, message, ) return utils.TestResult( 0, outstr, outerr, duration, config.classname, config.entry, tooluri, joburi, ) if bool(test.get("should_fail", False)): return utils.TestResult( 1, outstr, outerr, duration, config.classname, config.entry, tooluri, joburi, "Test should failed, but it did not.", ) fail_message = "" try: compare(test.get("output"), out) except CompareFail as ex: logger.warning("""Test failed: %s %s""", processfile, jobfile) logger.warning(test.get("doc")) logger.warning("Compare failure %s", ex) fail_message = str(ex) return utils.TestResult( (1 if fail_message else 0), outstr, outerr, duration, config.classname, config.entry, tooluri, joburi, fail_message, ) class CWLTestException(Exception): """custom exception for error reporting.""" class CWLItem(pytest.Item): """A CWL test Item.""" def __init__( self, name: str, parent: Optional["Node"], spec: dict[str, Any], ) -> None: """Initialize this CWLItem.""" super().__init__(name, parent) self.spec = spec def runtest(self) -> None: """Execute using cwltest.""" cwl_args = self.config.getoption("cwl_args") config = utils.CWLTestConfig( basedir=self.config.getoption("cwl_basedir"), test_baseuri=self.config.getoption("cwl_basedir"), test_basedir=self.config.getoption("cwl_basedir"), entry="tests.yaml", entry_line="0", outdir=str( self.config._tmp_path_factory.mktemp( # type: ignore[attr-defined] self.spec.get("label", "unlabled_test") ) ), tool=self.config.getoption("cwl_runner"), args=cwl_args.split(" ") if cwl_args else None, testargs=self.config.getoption("cwl_test_arg"), timeout=self.config.getoption("timeout", None), verbose=self.config.getoption("verbose", 0) >= 1, runner_quiet=not self.config.getoption("cwl_runner_verbose", False), ) hook = self.config.hook.pytest_cwl_execute_test result = _run_test_hook_or_plain( self.spec, config, hook, ) cwl_results = self.config.cwl_results # type: ignore[attr-defined] cast(list[tuple[dict[str, Any], utils.TestResult]], cwl_results).append( (self.spec, result) ) if result.return_code != 0: raise CWLTestException(self, result) def repr_failure( self, excinfo: pytest.ExceptionInfo[BaseException], style: Optional["TracebackStyle"] = None, ) -> str: """ Document failure reason. Called when self.runtest() raises an exception. """ if isinstance(excinfo.value, CWLTestException): from ruamel.yaml.main import YAML yaml = YAML() result = excinfo.value.args[1] stream = StringIO() yaml.dump(self.spec, stream) return "\n".join( [ "CWL test execution failed. ", result.message, f"Test: {stream.getvalue()}", ] ) else: return ( f"{excinfo.type.__name__} occurred during CWL test execution:\n" + "".join( traceback.format_exception( excinfo.type, excinfo.value, excinfo.traceback[0]._rawentry ) ) ) def reportinfo(self) -> tuple[Union["os.PathLike[str]", str], int | None, str]: """Status report.""" return self.path, 0, "cwl test: %s" % self.name class CWLYamlFile(pytest.File): """A CWL test file.""" def _add_global_properties(self) -> None: """Nonfunctional if xdist is installed and anything besides "-n 0" is used.""" from _pytest.junitxml import xml_key if xml := self.config._store.get(xml_key, None): xml.add_global_property("runner", self.config.getoption("cwl_runner")) xml.add_global_property( "runner_extra_args", self.config.getoption("cwl_args") ) 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( _get_comma_separated_option(self.config, "cwl_exclude_tags") ) tests, _ = utils.load_and_validate_tests(str(self.path)) self._add_global_properties() for entry in tests: entry_tags = entry.get("tags", []) if "label" in entry: name = entry["label"] elif "id" in entry: name = utils.shortname(str(entry["id"])) else: name = entry.get("doc", "") item = CWLItem.from_parent(self, name=name, spec=entry) if include and name not in include: item.add_marker( pytest.mark.skip( reason=f"Test {name!r} is not in the include list: {','.join(include)}." ) ) elif exclude and name in exclude: item.add_marker( pytest.mark.skip(reason=f"Test {name!r} is in the exclude list.") ) elif tags and not tags.intersection(entry_tags): item.add_marker( pytest.mark.skip( reason=f"Test {name!r} with tags {','.join(entry_tags)}" f" doesn't have a tag on the allowed tag list: {','.join(tags)}." ) ) elif exclude_tags and exclude_tags.intersection(entry_tags): item.add_marker( pytest.mark.skip( reason=f"Test {name!r} has one or more tags on the exclusion " f" tag list: {','.join(exclude_tags.intersection(entry_tags))}." ) ) yield item __OPTIONS: list[tuple[str, dict[str, Any]]] = [ ( "--cwl-runner", { "type": str, "dest": "cwl_runner", "default": "cwl-runner", "help": "Name of the CWL runner to use.", }, ), ( "--cwl-runner-verbose", { "dest": "cwl_runner_verbose", "default": False, "action": "store_true", "help": "If set, don't pass --quiet to the CWL runner.", }, ), ( "--cwl-badgedir", { "type": str, "help": "Create badge JSON files, one for each tag (plus a computed " "'all' tag), and store them in this directory.", }, ), ( "--cwl-include", { "type": str, "help": "Run specific CWL tests using their short names separated by comma", }, ), ( "--cwl-exclude", { "type": str, "help": "Exclude specific CWL tests using their short names separated by comma", }, ), ("--cwl-tags", {"type": str, "help": "Tags to be tested."}), ("--cwl-exclude-tags", {"type": str, "help": "Tags not to be tested."}), ( "--cwl-args", { "type": str, "help": "one or more arguments to pass first to tool runner (separated by spaces)", }, ), ( "--cwl-test-arg", { "type": str, "help": "Additional argument given in test cases and required prefix for tool runner.", "action": "append", }, ), ( "--cwl-basedir", { "help": "Basedir to use for tests", "default": os.getcwd(), }, ), ] 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]) def _doc_options() -> argparse.ArgumentParser: """Generate a stand-alone ArgumentParser to aid in documention.""" parser = argparse.ArgumentParser("cwltest options for pytest.", add_help=False) for entry in __OPTIONS: parser.add_argument(entry[0], **entry[1]) return parser def pytest_collect_file( file_path: Path, parent: pytest.Collector ) -> pytest.Collector | None: """Is this file for us.""" if ( file_path.suffix == ".yml" or file_path.suffix == ".yaml" ) and file_path.stem.endswith(".cwltest"): return cast( Optional[pytest.Collector], CWLYamlFile.from_parent(parent, path=file_path), ) return 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]] = [] config.cwl_results = cwl_results # type: ignore[attr-defined] 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, strict=True)) 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]], session.config.cwl_results, # type: ignore[attr-defined] ) 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, failures, unsupported, ntotal, npassed, nfailures, nunsupported, _, ) = utils.parse_results(results, tests) utils.generate_badges(cwl_badgedir, ntotal, npassed, nfailures, nunsupported) def pytest_addhooks(pluginmanager: pytest.PytestPluginManager) -> None: """Register our cwl hooks.""" from cwltest import hooks pluginmanager.add_hookspecs(hooks) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3958092 cwltest-2.7.20261106145043/src/cwltest/py.typed0000664000175000017500000000000015273371155020232 0ustar00michaelmichael././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3958092 cwltest-2.7.20261106145043/src/cwltest/stdfsaccess.py0000664000175000017500000000304515273371155021426 0ustar00michaelmichael"""Abstracted IO access.""" import os import urllib from typing import IO, Any from schema_salad.ref_resolver import uri_file_path def abspath(src: str, basedir: str) -> str: """Determine local filesystem absolute path given a basedir, handling both plain paths and URIs.""" if src.startswith("file://"): abpath = uri_file_path(src) elif urllib.parse.urlsplit(src).scheme in ["http", "https"]: return src else: if basedir.startswith("file://"): abpath = src if os.path.isabs(src) else basedir + "/" + src else: abpath = src if os.path.isabs(src) else os.path.join(basedir, src) return abpath class StdFsAccess: """Local filesystem implementation.""" def __init__(self, basedir: str) -> None: """Perform operations with respect to a base directory.""" self.basedir = basedir def _abs(self, p: str) -> str: return abspath(p, self.basedir) def open(self, fn: str, mode: str) -> IO[Any]: """Open a file from a file: URI.""" return open(self._abs(fn), mode) def size(self, fn: str) -> int: """Get the size of the file resource pointed to by a URI.""" return os.stat(self._abs(fn)).st_size def isfile(self, fn: str) -> bool: """Determine if a resource pointed to by a URI represents a file.""" return os.path.isfile(self._abs(fn)) def isdir(self, fn: str) -> bool: """Determine if a resource pointed to by a URI represents a directory.""" return os.path.isdir(self._abs(fn)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3958092 cwltest-2.7.20261106145043/src/cwltest/utils.py0000664000175000017500000005144315273371155020266 0ustar00michaelmichaelimport json import os import re import shlex import shutil import subprocess # nosec import sys import tempfile import time from collections import Counter, defaultdict from collections.abc import Iterable, MutableMapping, MutableSequence from importlib.metadata import EntryPoint, entry_points from importlib.resources import files from typing import Any, cast from urllib.parse import urljoin import junit_xml import ruamel.yaml.scanner import schema_salad.avro import schema_salad.ref_resolver import schema_salad.schema from rdflib import Graph from ruamel.yaml.scalarstring import ScalarString from schema_salad.exceptions import ValidationException import cwltest.compare import cwltest.stdfsaccess from cwltest import REQUIRED, UNSUPPORTED_FEATURE, logger, templock from cwltest.compare import CompareFail, compare class CWLTestConfig: """Store configuration values for cwltest.""" def __init__( self, entry: str, entry_line: str, basedir: str | None = None, test_baseuri: str | None = None, test_basedir: str | None = None, outdir: str | None = None, classname: str | None = None, tool: str | None = None, args: list[str] | None = None, testargs: list[str] | None = None, timeout: int | None = None, verbose: bool | None = None, runner_quiet: bool | None = None, ) -> None: """Initialize test configuration.""" self.basedir: str = basedir or os.getcwd() self.test_baseuri: str = test_baseuri or "file://" + self.basedir self.test_basedir: str = test_basedir or self.basedir self.outdir: str | None = outdir self.classname: str = classname or "" self.entry = urljoin( 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.timeout: int | None = timeout self.verbose: bool = verbose or False self.runner_quiet: bool = runner_quiet or True class CWLTestReport: """Encapsulate relevant test result data for a markdown report.""" def __init__( self, id: int | str, category: list[str], entry: str, tool: str, job: str | None, ) -> None: """Initialize a CWLTestReport object.""" self.id = id self.category = category self.entry = entry self.tool = tool self.job = job class TestResult: """Encapsulate relevant test result data.""" def __init__( self, return_code: int, standard_output: str, error_output: str, duration: float, classname: str, entry: str, tool: str, job: str | None, message: str = "", ) -> None: """Initialize a TestResult object.""" self.return_code = return_code self.standard_output = standard_output self.error_output = error_output self.duration = duration self.message = message self.classname = classname self.entry = entry self.tool = tool self.job = job 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"): category = ", ".join(test["tags"]) else: category = REQUIRED short_name = test.get("short_name") case = junit_xml.TestCase( doc, elapsed_sec=self.duration, file=short_name, category=category, stdout=self.standard_output, stderr=self.error_output, ) if self.return_code > 0: case.failure_message = self.message return case 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"]), self.entry, self.tool, self.job, ) def _clean_ruamel_list(obj: list[Any]) -> Any: """Entrypoint to transform roundtrip loaded ruamel.yaml to plain objects.""" new_list = [] for entry in obj: e: Any = _clean_ruamel(entry) e["line"] = str(entry.lc.line) new_list.append(e) return new_list def _clean_ruamel(obj: Any) -> Any: """Transform roundtrip loaded ruamel.yaml to plain objects.""" if isinstance(obj, MutableMapping): new_dict = {} for k, v in obj.items(): new_dict[str(k)] = _clean_ruamel(v) return new_dict if isinstance(obj, MutableSequence): new_list = [] for entry in obj: new_list.append(_clean_ruamel(entry)) return new_list if isinstance(obj, ScalarString): return str(obj) for typ in int, float, bool, str: if isinstance(obj, typ): return typ(obj) if obj is None: return None raise Exception(f"Unsupported type {type(obj)} of {obj!r}.") def generate_badges( badgedir: str, 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) for t, v in ntotal.items(): percent = int((len(npassed[t]) / float(v)) * 100) if len(npassed[t]) == v: color = "green" elif t == "required": color = "red" else: color = "yellow" with open(f"{badgedir}/{t}.json", "w") as out: out.write( json.dumps( { "subject": f"{t}", "status": f"{percent}%", "color": color, } ) ) 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(str(e.id))}]({e.entry})" tool = f"[tool]({e.tool})" if e.job: arr = [tool, f"[job]({e.job})"] else: arr = [tool] args = ", ".join(arr) print(f"- {base} ({args})", file=out) print("## List of failed tests", file=out) for e in nfailures[t]: base = f"[{shortname(str(e.id))}]({e.entry})" tool = f"[tool]({e.tool})" if e.job: arr = [tool, f"[job]({e.job})"] else: arr = [tool] args = ", ".join(arr) print(f"- {base} ({args})", file=out) print("## List of unsupported tests", file=out) for e in nunsupported[t]: base = f"[{shortname(str(e.id))}]({e.entry})" tool = f"[tool]({e.tool})" if e.job: arr = [tool, f"[job]({e.job})"] else: arr = [tool] args = ", ".join(arr) print(f"- {base} ({args})", file=out) def get_test_number_by_key( tests: list[dict[str, str]], key: str, value: str ) -> int | None: """Retrieve the test index from its name.""" for i, test in enumerate(tests): if key in test and test[key] == value: return i return None def load_and_validate_tests(path: str) -> tuple[Any, dict[str, Any]]: """ Load and validate the given test file against the cwltest schema. This also processes $import directives. """ schema_resource = files("cwltest").joinpath("cwltest-schema.yml") with schema_resource.open("r", encoding="utf-8") as fp: cache: dict[str, str | Graph | bool] | None = { "https://w3id.org/cwl/cwltest/cwltest-schema.yml": fp.read() } ( document_loader, avsc_names, _, _, ) = schema_salad.schema.load_schema( "https://w3id.org/cwl/cwltest/cwltest-schema.yml", cache=cache ) if not isinstance(avsc_names, schema_salad.avro.schema.Names): print(avsc_names) raise ValidationException(f"Wrong instance for avsc_names: {type(avsc_names)}") tests, metadata = schema_salad.schema.load_and_validate( document_loader, avsc_names, path, True ) tests = cast(list[dict[str, Any]], _clean_ruamel_list(tests)) return tests, metadata def parse_results( results: Iterable[TestResult], tests: list[dict[str, Any]], suite_name: str | None = None, report: junit_xml.TestSuite | None = None, ) -> 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 junit_xml.TestSuite | None, ]: """ 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. """ total = 0 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) for i, test_result in enumerate(results): test_case = test_result.create_test_case(tests[i]) test_report = test_result.create_report_entry(tests[i]) test_case.url = ( f"cwltest:{suite_name}#{i + 1}" if suite_name is not None else "cwltest:#{i + 1}" ) total += 1 tags = tests[i].get("tags", []) + ["all"] for t in tags: ntotal[t] += 1 return_code = test_result.return_code category = test_case.category if return_code == 0: passed += 1 for t in tags: npassed[t].append(test_report) elif return_code != 0 and return_code != UNSUPPORTED_FEATURE: failures += 1 for t in tags: nfailures[t].append(test_report) test_case.add_failure_info(output=test_result.message) elif category != REQUIRED and return_code == UNSUPPORTED_FEATURE: unsupported += 1 for t in tags: nunsupported[t].append(test_report) test_case.add_skipped_info("Unsupported") else: raise Exception( "This is impossible, return_code: {}, category: " "{}".format(return_code, category) ) if report: report.test_cases.append(test_case) return ( total, passed, failures, unsupported, ntotal, npassed, nfailures, nunsupported, report, ) def prepare_test_command( tool: str, args: list[str], testargs: list[str] | None, test: dict[str, Any], cwd: str, quiet: bool | None = True, outdir: str | None = None, ) -> tuple[list[str], str | None]: """Turn the test into a command line.""" test_command = [tool] test_command.extend(args) # Add additional arguments given in test case if testargs is not None: for testarg in testargs: test_case_name, prefix = testarg.split("==") if test_case_name in test: test_command.extend([prefix, test[test_case_name]]) if outdir: os.makedirs(outdir, exist_ok=True) outdir = tempfile.mkdtemp(dir=outdir) else: # Add prefixes if running on MacOSX so that boot2docker writes to /Users with templock: if "darwin" in sys.platform and tool.endswith("cwltool"): outdir = tempfile.mkdtemp(prefix=os.path.abspath(os.path.curdir)) test_command.extend( [ f"--tmp-outdir-prefix={outdir}", f"--tmpdir-prefix={outdir}", ] ) else: outdir = tempfile.mkdtemp() test_command.extend([f"--outdir={outdir}"]) if quiet: test_command.extend(["--quiet"]) processfile, jobfile = prepare_test_paths(test, cwd) test_command.extend([os.path.normcase(processfile)]) if jobfile: test_command.append(os.path.normcase(jobfile)) return test_command, outdir def prepare_test_paths( test: dict[str, str], cwd: str, ) -> tuple[str, str | None]: """Determine the test path and the tool path.""" cwd = schema_salad.ref_resolver.file_uri(cwd) processfile = test["tool"] if processfile.startswith(cwd): processfile = processfile[len(cwd) + 1 :] jobfile = test.get("job") if jobfile: if jobfile.startswith(cwd): jobfile = jobfile[len(cwd) + 1 :] return processfile, jobfile def run_test_plain( config: CWLTestConfig, test: dict[str, str], test_number: int | None = None, ) -> TestResult: """Plain test runner.""" out: dict[str, Any] = {} outstr = outerr = "" test_command: list[str] = [] duration = 0.0 number = "?" reltool = os.path.relpath(test["tool"], start=config.test_basedir) tooluri = urljoin(config.test_baseuri, reltool) if test.get("job", None): reljob = os.path.relpath(test["job"], start=config.test_basedir) joburi = urljoin(config.test_baseuri, reljob) else: joburi = None if test_number is not None: number = str(test_number) process: subprocess.Popen[str] | None = None test_outdir: str | None = None try: cwd = os.getcwd() test_command, test_outdir = prepare_test_command( config.tool, config.args, config.testargs, test, cwd, config.runner_quiet, config.outdir, ) if config.verbose: sys.stderr.write(f"Running: {' '.join(test_command)}\n") sys.stderr.flush() start_time = time.time() stderr = subprocess.PIPE if not config.verbose else None process = subprocess.Popen( # nosec test_command, stdout=subprocess.PIPE, stderr=stderr, universal_newlines=True, cwd=cwd, ) outstr, outerr = process.communicate(timeout=config.timeout) return_code = process.poll() duration = time.time() - start_time if return_code: raise subprocess.CalledProcessError(return_code, " ".join(test_command)) logger.debug('outstr: "%s".', outstr) out = json.loads(outstr) if outstr else {} except subprocess.CalledProcessError as err: if err.returncode == UNSUPPORTED_FEATURE and REQUIRED not in test.get( "tags", ["required"] ): return TestResult( UNSUPPORTED_FEATURE, outstr, outerr, duration, config.classname, config.entry, tooluri, joburi, ) if test.get("should_fail", False): return TestResult( 0, outstr, outerr, duration, config.classname, config.entry, tooluri, joburi, ) if test_number: logger.error( """Test %i failed: %s""", test_number, shlex.join(test_command), ) else: logger.error( """Test failed: %s""", shlex.join(test_command), ) logger.error(test.get("doc", "").replace("\n", " ").strip()) if err.returncode == UNSUPPORTED_FEATURE: logger.error("Does not support required feature") else: logger.error("Returned non-zero") return TestResult( 1, outstr, outerr, duration, config.classname, config.entry, tooluri, joburi, str(err), ) except (ruamel.yaml.scanner.ScannerError, TypeError) as err: logger.error( """Test %s failed: %s""", number, shlex.join(test_command), ) logger.error(outstr) logger.error("Parse error %s", str(err)) logger.error(outerr) except KeyboardInterrupt: logger.error( """Test %s interrupted: %s""", number, shlex.join(test_command), ) raise except json.JSONDecodeError: logger.error( """Test %s failed: %s""", number, shlex.join(test_command), ) logger.error(test.get("doc", "").replace("\n", " ").strip()) invalid_json_msg = "Output is not a valid JSON document: '%s'" % outstr logger.error(invalid_json_msg) return TestResult( 1, outstr, outerr, duration, config.classname, config.entry, tooluri, joburi, invalid_json_msg, ) except subprocess.TimeoutExpired: logger.error( """Test %s timed out: %s""", number, 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 # instructed in the subprocess docs. if process: process.kill() outstr, outerr = process.communicate() return TestResult( 2, outstr, outerr, float(cast(int, config.timeout)), config.classname, config.entry, tooluri, joburi, "Test timed out", ) finally: if process is not None and process.returncode is None: logger.error("""Terminating lingering process""") process.terminate() for _ in range(0, 3): time.sleep(1) if process.poll() is not None: break if process.returncode is None: process.kill() fail_message = "" if test.get("should_fail", False): logger.warning( """Test %s failed: %s""", number, shlex.join(test_command), ) logger.warning(test.get("doc", "").replace("\n", " ").strip()) logger.warning("Returned zero but it should be non-zero") return TestResult( 1, outstr, outerr, duration, config.classname, config.entry, tooluri, joburi, ) try: compare(test.get("output"), out) except CompareFail as ex: logger.warning( """Test %s failed: %s""", number, shlex.join(test_command), ) logger.warning(test.get("doc", "").replace("\n", " ").strip()) logger.warning("Compare failure %s", ex) fail_message = str(ex) if config.outdir and test_outdir: shutil.rmtree(test_outdir, True) return TestResult( (1 if fail_message else 0), outstr, outerr, duration, config.classname, config.entry, tooluri, joburi, fail_message, ) def shortname(name: str) -> str: """ Return the short name of a given name. It is a workaround of https://github.com/common-workflow-language/schema_salad/issues/511. """ return [n for n in re.split("[/#]", name) if len(n)][-1] def absuri(path: str) -> str: """Return an absolute URI.""" if "://" in path: return path return "file://" + os.path.abspath(path) def load_optional_fsaccess_plugin() -> None: """ Load optional fsaccess plugin. Looks for a package with cwltest.fsaccess entry point and if so, use that to get a filesystem access object that will be used for checking test output. """ fsaccess_eps: tuple[EntryPoint, ...] = tuple(entry_points(group="cwltest.fsaccess")) if len(fsaccess_eps) == 0: return if len(fsaccess_eps) > 1: logger.warning( "More than one cwltest.fsaccess entry point found, selected %s", fsaccess_eps[0], ) cwltest.compare.fs_access = fsaccess_eps[0].load()() ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1793979163.619293 cwltest-2.7.20261106145043/tests/0000775000175000017500000000000015273371434015433 5ustar00michaelmichael././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3958092 cwltest-2.7.20261106145043/tests/__init__.py0000664000175000017500000000003115273371155017536 0ustar00michaelmichael"""Tests for cwltest.""" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1793979163.622734 cwltest-2.7.20261106145043/tests/test-data/0000775000175000017500000000000015273371434017321 5ustar00michaelmichael././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3962986 cwltest-2.7.20261106145043/tests/test-data/badgedir.yaml0000664000175000017500000000151015273371155021743 0ustar00michaelmichael- output: {} job: empty.yml tool: return-0.cwl id: success_w_job doc: Successful test with a job file tags: [ command_line_tool ] - output: {} tool: return-0.cwl id: success_wo_job doc: Successful test without a job file tags: [ command_line_tool ] - output: {} job: empty.yml tool: return-1.cwl id: failure_w_job doc: Failed test with a job file tags: [ command_line_tool ] - output: {} tool: return-1.cwl id: failure_wo_job doc: Failed test without a job file tags: [ command_line_tool ] - output: {} job: empty.yml tool: return-unsupported.cwl id: unsupported_w_job doc: Unsupported test with a job file tags: [ command_line_tool ] - output: {} job: null tool: return-unsupported.cwl id: unsupported_wo_job doc: Unsupported test without a job file tags: [ command_line_tool ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/conformance_test_v1.0.cwltest.yml0000664000175000017500000000071615273371155025631 0ustar00michaelmichael- output: args: [cat, hello.txt] job: v1.0/cat-job.json tool: v1.0/cat1-testcli.cwl label: cl_optional_inputs_missing id: 4 doc: Test command line with optional input (missing) tags: [ required, command_line_tool ] - output: args: [cat, -n, hello.txt] job: v1.0/cat-n-job.json tool: v1.0/cat1-testcli.cwl label: cl_optional_bindings_provided id: 5 doc: Test command line with optional input (provided) tags: [ command_line_tool ]././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/conformance_test_v1.2.cwltest.yaml0000664000175000017500000000121015273371155025762 0ustar00michaelmichael- output: args: [cat, hello.txt] job: v1.0/cat-job.json tool: v1.0/cat1-testcli.cwl id: cl_optional_inputs_missing doc: Test command line with optional input (missing) tags: [ required, command_line_tool ] - output: args: [cat, -n, hello.txt] job: v1.0/cat-n-job.json tool: v1.0/cat1-testcli.cwl id: cl_optional_bindings_provided doc: Test command line with optional input (provided) tags: [ command_line_tool ] - tool: v1.0/null-expression2-tool.cwl should_fail: true id: expression_any_nodefaultany doc: Test Any without defaults cannot be unspecified. tags: [ required, inline_javascript, expression_tool ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/cores.txt0000664000175000017500000000000215273371155021165 0ustar00michaelmichael2 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/cwltool-conftest.py0000664000175000017500000000206515273371155023204 0ustar00michaelmichael""" Example configuration for pytest-cwl plugin using cwltool directly. Calls cwltool via Python, instead of a subprocess via `--cwl-runner cwltool`. """ import json from io import StringIO from typing import Any, Dict, List, Optional, Tuple from cwltest import utils def pytest_cwl_execute_test( config: utils.CWLTestConfig, processfile: str, jobfile: Optional[str] ) -> Tuple[int, Optional[Dict[str, Any]]]: """Use the CWL reference runner (cwltool) to execute tests.""" from cwltool import main from cwltool.errors import WorkflowException stdout = StringIO() argsl: List[str] = [f"--outdir={config.outdir}"] if config.runner_quiet: argsl.append("--quiet") elif config.verbose: argsl.append("--debug") argsl.extend(config.args) argsl.append(processfile) if jobfile: argsl.append(jobfile) try: result = main.main(argsl=argsl, stdout=stdout) except WorkflowException: return 1, {} out = stdout.getvalue() return result, json.loads(out) if out else {} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/dummy-executor.sh0000775000175000017500000000005115273371155022643 0ustar00michaelmichael#!/bin/sh echo "it is not JSON format!" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/empty.yml0000664000175000017500000000000315273371155021173 0ustar00michaelmichael{} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/exclude-tags.yml0000664000175000017500000000065015273371155022432 0ustar00michaelmichael- job: v1.0/cat-job.json output: {} tool: return-0.cwl doc: | Test with label id: opt-error1 tags: [ command_line_tool ] - job: v1.0/cat-job.json id: opt-error2 output: {} tool: return-0.cwl doc: | Test without label tags: [ command_line_tool, workflow ] - job: v1.0/cat-job.json id: opt-error3 output: {} tool: return-0.cwl doc: | Test without label tags: [ workflow ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/integer-id.cwltest.yml0000664000175000017500000000030315273371155023553 0ustar00michaelmichael- output: args: [cat, hello.txt] job: v1.0/cat-job.json tool: v1.0/cat1-testcli.cwl id: 4 doc: Test command line with optional input (missing) tags: [ required, command_line_tool ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/integer-id.yml0000664000175000017500000000014315273371155022071 0ustar00michaelmichael- job: v1.0/cat1-job.json output: {} tool: return-0.cwl doc: Test with an integer id id: 1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/mock_cwl_runner.py0000775000175000017500000000200015273371155023055 0ustar00michaelmichael#!/usr/bin/env python3 """Mock implementation of the cwlref-runner command line.""" import argparse import sys from time import sleep from cwltest import UNSUPPORTED_FEATURE UNSUPPORTED_FEATURE_TOOL = "return-unsupported.cwl" ERROR_TOOL = "return-1.cwl" TIMEOUT_TOOL = "timeout.cwl" def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("processfile") parser.add_argument("jobfile", nargs='?', default=None) parser.add_argument("--version", action="version", version="%(prog)s 1.0") parser.add_argument("--outdir") parser.add_argument("--quiet", action="store_true") args = parser.parse_args() if args.processfile.endswith(UNSUPPORTED_FEATURE_TOOL): exit(UNSUPPORTED_FEATURE) elif args.processfile.endswith(ERROR_TOOL): exit(1) elif args.processfile.endswith(TIMEOUT_TOOL): print("timeout stderr", file=sys.stderr) sys.stderr.flush() sleep(100000) exit(1) exit(0) if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/multi-lined-doc.yml0000664000175000017500000000041215273371155023027 0ustar00michaelmichael- job: v1.0/cat-job.json output: {} tool: return-0.cwl doc: | Test with label label: opt-error tags: [ js, init_work_dir ] - job: v1.0/cat-job.json output: {} tool: return-0.cwl doc: | Test without label tags: [ js, init_work_dir ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/nothing.yml0000664000175000017500000000030615273371155021511 0ustar00michaelmichael- job: empty.yml tool: true.cwl output: {} id: do_nothing doc: Example of doing nothing - job: empty.yml tool: true.cwl output: {} id: do_nothing2 doc: Example of doing nothing more ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/optional-error.yml0000664000175000017500000000044415273371155023022 0ustar00michaelmichael- job: v1.0/cat-job.json output: output_txt: class: File checksum: sha1$47a013e660d408619d894b20806b1d5086aab03b location: output.txt size: 13 tool: return-1.cwl doc: Optional test that returns error code other than unsupported tags: [ js, init_work_dir ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/optional-unsupported.yml0000664000175000017500000000041215273371155024254 0ustar00michaelmichael- job: v1.0/cat-job.json output: output_txt: class: File checksum: sha1$47a013e660d408619d894b20806b1d5086aab03b location: output.txt size: 13 tool: return-unsupported.cwl doc: Optional test that is unsupported tags: [ optional ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/required-unsupported.yml0000664000175000017500000000103115273371155024245 0ustar00michaelmichael- job: v1.0/cat-job.json output: output_txt: class: File checksum: sha1$47a013e660d408619d894b20806b1d5086aab03b location: output.txt size: 13 tool: return-unsupported.cwl doc: Required test that is unsupported (without tags) - job: v1.0/cat-job.json output: output_txt: class: File checksum: sha1$47a013e660d408619d894b20806b1d5086aab03b location: output.txt size: 13 tool: return-unsupported.cwl doc: Required test that is unsupported (with tags) tags: [ required ]././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/return-0.cwl0000664000175000017500000000000015273371155021472 0ustar00michaelmichael././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/return-1.cwl0000664000175000017500000000000015273371155021473 0ustar00michaelmichael././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/return-unsupported.cwl0000664000175000017500000000000015273371155023723 0ustar00michaelmichael././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/short-names.yml0000664000175000017500000000021215273371155022277 0ustar00michaelmichael- job: v1.0/cat-job.json output: {} tool: return-0.cwl doc: Test with a short name label: opt-error tags: [ js, init_work_dir ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/string-id.yml0000664000175000017500000000016115273371155021742 0ustar00michaelmichael- job: v1.0/cat1-job.json output: {} tool: return-0.cwl doc: Test with a string label id: test-string-id ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/timeout.cwl0000664000175000017500000000013215273371155021512 0ustar00michaelmichaelclass: CommandLineTool cwlVersion: v1.0 inputs: [] outputs: [] baseCommand: [sleep, "15"] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/timeout.yml0000664000175000017500000000036015273371155021531 0ustar00michaelmichael- job: v1.0/empty.json output: output_txt: class: File checksum: sha1$47a013e660d408619d894b20806b1d5086aab03b location: output.txt size: 13 tool: timeout.cwl doc: Test of timeout stdout and stderr capture ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/true.cwl0000664000175000017500000000012515273371155021005 0ustar00michaelmichaelclass: CommandLineTool cwlVersion: v1.0 inputs: {} outputs: {} baseCommand: ["true"] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793979163.6237962 cwltest-2.7.20261106145043/tests/test-data/v1.0/0000775000175000017500000000000015273371434020005 5ustar00michaelmichael././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3963344 cwltest-2.7.20261106145043/tests/test-data/v1.0/args.py0000664000175000017500000000026415273371155021315 0ustar00michaelmichael#!/usr/bin/env python3 import sys import json import os args = [os.path.basename(a) for a in sys.argv[1:]] with open("cwl.output.json", "w") as f: json.dump({"args": args}, f) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test-data/v1.0/cat-job.json0000664000175000017500000000012115273371155022211 0ustar00michaelmichael{ "file1": { "class": "File", "location": "hello.txt" } }././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test-data/v1.0/cat-n-job.json0000664000175000017500000000015015273371155022446 0ustar00michaelmichael{ "file1": { "class": "File", "location": "hello.txt" }, "numbering": true }././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test-data/v1.0/cat1-job.json0000664000175000017500000000000015273371155022266 0ustar00michaelmichael././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test-data/v1.0/cat1-testcli.cwl0000775000175000017500000000203215273371155023011 0ustar00michaelmichael#!/usr/bin/env cwl-runner { "class": "CommandLineTool", "cwlVersion": "v1.0", "doc": "Print the contents of a file to stdout using 'cat' running in a docker container.", "hints": [ { "class": "DockerRequirement", "dockerPull": "python:3-slim" }, { "class": "ResourceRequirement", "ramMin": 128 } ], "inputs": [ { "id": "file1", "type": "File", "inputBinding": {"position": 1} }, { "id": "numbering", "type": ["null", "boolean"], "inputBinding": { "position": 0, "prefix": "-n" } }, { id: "args.py", type: File, default: { class: File, location: args.py }, inputBinding: { position: -1 } } ], "outputs": [{"id": "args", "type": "string[]"}], "baseCommand": "python3", "arguments": ["cat"] } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test-data/v1.0/cat2-job.json0000664000175000017500000000000015273371155022267 0ustar00michaelmichael././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test-data/v1.0/empty.json0000664000175000017500000000000315273371155022027 0ustar00michaelmichael{} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test-data/v1.0/hello.txt0000664000175000017500000000001415273371155021644 0ustar00michaelmichaelHello world!././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test-data/v1.0/null-expression2-tool.cwl0000664000175000017500000000034415273371155024721 0ustar00michaelmichael#!/usr/bin/env cwl-runner class: ExpressionTool requirements: - class: InlineJavascriptRequirement cwlVersion: v1.2 inputs: i1: Any outputs: output: int expression: "$({'output': (inputs.i1 == 'the-default' ? 1 : 2)})"././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test-data/with-and-without-short-names.yml0000664000175000017500000000031715273371155025517 0ustar00michaelmichael- job: v1.0/cat1-job.json output: {} tool: return-0.cwl doc: Test without a short name - job: v1.0/cat2-job.json output: {} tool: return-0.cwl doc: Test with a short name short_name: opt-error ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test_argparse.py0000664000175000017500000000267015273371155020655 0ustar00michaelmichaelimport sys from typing import TYPE_CHECKING from unittest.mock import patch import pytest from cwltest.argparser import arg_parser from cwltest.main import main if TYPE_CHECKING: from pathlib import Path def test_arg() -> None: """Basic test of the argparse.""" parser = arg_parser() parsed = parser.parse_args( ["--test", "test_name", "-n", "52", "--tool", "cwltool", "-j", "4"] ) assert parsed.test == "test_name" assert parsed.n == "52" assert parsed.tool == "cwltool" assert parsed.j == 4 @pytest.mark.parametrize("verbose", [False, True]) def test_invalid_test_file(verbose: bool, tmp_path: "Path") -> None: argv = ["cwltest", "--test", str(tmp_path / "invalid_file.abc.txt")] if verbose: argv.append("--verbose") with ( patch("cwltest.main.logger") as mocked_log, patch.object(sys, "argv", argv), ): assert main() == 1 mocked_log.error.assert_called_once_with( f"Failed validating test file: {str(tmp_path / 'invalid_file.abc.txt')}", exc_info=verbose, ) def test_outdir() -> None: parser = arg_parser() parsed = parser.parse_args( ["--test", "test_name", "--outdir", "/gpfs/scratch/user/bla/tmp"] ) assert parsed.outdir == "/gpfs/scratch/user/bla/tmp" def test_outdir_default() -> None: parser = arg_parser() parsed = parser.parse_args(["--test", "test_name"]) assert parsed.outdir is None ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test_badgedir.py0000664000175000017500000000672215273371155020614 0ustar00michaelmichaelimport json import os from pathlib import Path from textwrap import dedent import schema_salad.ref_resolver from .util import get_data, run_with_mock_cwl_runner def test_badgedir(tmp_path: Path) -> None: badgedir = tmp_path / "badgedir" args = [ "--test", schema_salad.ref_resolver.file_uri( get_data("tests/test-data/conformance_test_v1.2.cwltest.yaml") ), "--badgedir", str(badgedir), ] cwd = os.getcwd() try: os.chdir(get_data("tests/test-data/")) error_code, stdout, stderr = run_with_mock_cwl_runner(args) finally: os.chdir(cwd) assert error_code == 1 required_json = badgedir / "required.json" assert required_json.exists() with open(required_json) as file: obj = json.load(file) assert obj.get("subject", "") == "required" assert obj.get("status", "") == "0%" assert obj.get("color", "") == "red" required_md = badgedir / "required.md" assert required_md.exists() with open(required_md) as file: s = file.read() assert "file://" in s assert "tests/test-data/conformance_test_v1.2.cwltest.yaml" in s assert "v1.0/cat-job.json" in s assert "v1.0/cat1-testcli.cwl" in s clt = badgedir / "command_line_tool.json" assert clt.exists() with open(clt) as file: obj = json.load(file) assert obj.get("subject", "") == "command_line_tool" assert obj.get("status", "") == "0%" 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" baseuri = "https://example.com/specified/uri" args = [ "--test", schema_salad.ref_resolver.file_uri(get_data("tests/test-data/badgedir.yaml")), "--badgedir", str(badgedir), "--baseuri", baseuri, ] cwd = os.getcwd() try: os.chdir(get_data("tests/test-data/")) error_code, stdout, stderr = run_with_mock_cwl_runner(args) finally: os.chdir(cwd) clt_md = badgedir / "command_line_tool.md" assert clt_md.exists() with open(clt_md) as file: contents = file.read() assert contents == markdown_report_with(baseuri) def markdown_report_with(baseuri: str) -> str: return dedent(f""" # `command_line_tool` tests ## List of passed tests - [success_w_job]({baseuri}/badgedir.yaml#L0) ([tool]({baseuri}/return-0.cwl), [job]({baseuri}/empty.yml)) - [success_wo_job]({baseuri}/badgedir.yaml#L7) ([tool]({baseuri}/return-0.cwl)) ## List of failed tests - [failure_w_job]({baseuri}/badgedir.yaml#L13) ([tool]({baseuri}/return-1.cwl), [job]({baseuri}/empty.yml)) - [failure_wo_job]({baseuri}/badgedir.yaml#L20) ([tool]({baseuri}/return-1.cwl)) ## List of unsupported tests - [unsupported_w_job]({baseuri}/badgedir.yaml#L26) ([tool]({baseuri}/return-unsupported.cwl), [job]({baseuri}/empty.yml)) - [unsupported_wo_job]({baseuri}/badgedir.yaml#L33) ([tool]({baseuri}/return-unsupported.cwl)) """)[1:] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test_categories.py0000664000175000017500000000657715273371155021210 0ustar00michaelmichaelimport os import re from os import linesep as n from os import sep as p from pathlib import Path import defusedxml.ElementTree as ET import schema_salad.ref_resolver from .util import get_data, run_with_mock_cwl_runner def test_unsupported_with_required_tests() -> None: cwl_runner = get_data("tests/test-data/mock_cwl_runner.py") cwd = os.getcwd() os.chdir(get_data("tests/test-data/")) args = ["--test", "required-unsupported.yml"] try: error_code, stdout, stderr = run_with_mock_cwl_runner(args) finally: os.chdir(cwd) assert error_code == 1 print(stderr) stderr = re.sub(r" '?--outdir=[^ ]*", "", stderr) if os.name == "nt": q = "'" else: q = "" assert ( f"The `id` field is missing.{n}" f"The `id` field is missing.{n}" f"Test [1/2] Required test that is unsupported (without tags){n}" f"{n}" f"Test 1 failed: {cwl_runner} --quiet return-unsupported.cwl {q}v1.0{p}cat-job.json{q}{n}" f"Required test that is unsupported (without tags){n}" f"Does not support required feature{n}" f"Test [2/2] Required test that is unsupported (with tags){n}" f"{n}" f"Test 2 failed: {cwl_runner} --quiet return-unsupported.cwl {q}v1.0{p}cat-job.json{q}{n}" f"Required test that is unsupported (with tags){n}" f"Does not support required feature{n}" f"0 tests passed, 2 failures, 0 unsupported features{n}" ) == stderr def test_unsupported_with_optional_tests() -> None: args = [ "--test", schema_salad.ref_resolver.file_uri( get_data("tests/test-data/optional-unsupported.yml") ), ] error_code, stdout, stderr = run_with_mock_cwl_runner(args) stderr = re.sub(r" '?--outdir=[^ ]*", "", stderr) assert error_code == 0 assert ( "The `id` field is missing.{n}" "Test [1/1] Optional test that is unsupported{n}{n}" "0 tests passed, 1 unsupported " "features{n}".format(n=n) ) == stderr def test_error_with_optional_tests() -> None: args = [ "--test", schema_salad.ref_resolver.file_uri( get_data("tests/test-data/optional-error.yml") ), ] error_code, stdout, stderr = run_with_mock_cwl_runner(args) assert error_code == 1 assert "1 failures" in stderr def test_category_in_junit_xml(tmp_path: Path) -> None: junit_xml_report = tmp_path / "junit-report.xml" args = [ "--test", schema_salad.ref_resolver.file_uri( get_data("tests/test-data/optional-error.yml") ), "--junit-xml", str(junit_xml_report), ] run_with_mock_cwl_runner(args) tree = ET.parse(junit_xml_report) 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 """ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test_compare.py0000664000175000017500000004040615273371155020476 0ustar00michaelmichaelimport os from pathlib import Path from typing import Any import pytest from cwltest.compare import CompareFail, _compare_directory, _compare_file, compare from .util import get_data def test_compare_any_success() -> None: expected = "Any" actual: dict[str, Any] = {} compare(expected, actual) def test_compare_contents_failure() -> None: expected = { "location": "cores.txt", "size": 2, "class": "File", "contents": "2", } actual = { "basename": "cores.txt", "checksum": "sha1$7448d8798a4380162d4b56f9b452e2f6f9e24e7a", "class": "File", "location": "file:///var/folders/8x/2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/cores.txt", "path": get_data("tests/test-data/cores.txt"), "size": 2, } with pytest.raises(CompareFail): compare(expected, actual) def test_compare_contents_success() -> None: expected = { "location": "cores.txt", "size": 2, "class": "File", "contents": "2\n", "checksum": "sha1$7448d8798a4380162d4b56f9b452e2f6f9e24e7a", } actual = { "basename": "cores.txt", "checksum": "sha1$7448d8798a4380162d4b56f9b452e2f6f9e24e7a", "class": "File", "location": "file:///var/folders/8x/2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/cores.txt", "path": get_data("tests/test-data/cores.txt"), "size": 2, } compare(expected, actual) def test_compare_contents_not_exist() -> None: expected = { "location": "cores.txt", "class": "File", } actual = { "basename": "cores.txt", "class": "File", "location": "file:///var/folders/8x/2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/cores.txt", "path": "/none/exist/path/to/cores.txt", "size": 2, } with pytest.raises(CompareFail): _compare_file(expected, actual, False) def test_compare_file_different_size(tmp_path: Path) -> None: expected = { "location": "cores.txt", "size": 2, "class": "File", } path = tmp_path / "cores.txt" with open(path, "w") as f: f.write("hello") actual = { "basename": "cores.txt", "class": "File", "location": path.as_uri(), } with pytest.raises(CompareFail): _compare_file(expected, actual, False) def test_compare_file_different_checksum(tmp_path: Path) -> None: expected = { "location": "cores.txt", "class": "File", "checksum": "sha1$7448d8798a4380162d4b56f9b452e2f6f9e24e7a", } path = tmp_path / "cores.txt" with open(path, "w") as f: f.write("hello") actual = { "basename": "cores.txt", "class": "File", "location": path.as_uri(), } with pytest.raises(CompareFail): _compare_file(expected, actual, False) def test_compare_file_inconsistent_size(tmp_path: Path) -> None: expected = { "location": "cores.txt", "class": "File", } path = tmp_path / "cores.txt" with open(path, "w") as f: f.write("hello") actual = { "basename": "cores.txt", "class": "File", "location": path.as_uri(), "size": 65535, } with pytest.raises(CompareFail): _compare_file(expected, actual, False) def test_compare_file_inconsistent_checksum(tmp_path: Path) -> None: expected = { "location": "cores.txt", "class": "File", } path = tmp_path / "cores.txt" with open(path, "w") as f: f.write("hello") actual = { "basename": "cores.txt", "checksum": "inconsistent-checksum", "class": "File", "location": path.as_uri(), } with pytest.raises(CompareFail): _compare_file(expected, actual, False) def test_compare_directory(tmp_path: Path) -> None: expected = { "location": "dir", "class": "Directory", "listing": [], } path = tmp_path / "dir" os.makedirs(path) actual = { "class": "Directory", "location": path.as_uri(), "listing": [], } _compare_directory(expected, actual, False) def test_compare_directory_path(tmp_path: Path) -> None: expected = { "location": "dir", "class": "Directory", "listing": [], } path = tmp_path / "dir" os.makedirs(path) actual = { "class": "Directory", "path": str(path), "listing": [], } _compare_directory(expected, actual, False) def test_compare_directory_success() -> None: expected = { "stuff": { "class": "Directory", "listing": [ { "basename": "baz.txt", "checksum": "sha1$da39a3ee5e6b4b0d3255bfef95601890afd80709", "class": "File", "size": 0, }, { "basename": "foo", "class": "Directory", "listing": [ { "basename": "bar.txt", "checksum": "sha1$da39a3ee5e6b4b0d3255bfef95601890afd80709", "class": "File", "size": 0, } ], }, ], } } actual = { "stuff": { "class": "Directory", "listing": [ { "basename": "baz.txt", "checksum": "sha1$da39a3ee5e6b4b0d3255bfef95601890afd80709", "class": "File", "size": 0, }, { "basename": "foo", "class": "Directory", "listing": [ { "basename": "bar.txt", "checksum": "sha1$da39a3ee5e6b4b0d3255bfef95601890afd80709", "class": "File", "size": 0, } ], }, ], } } compare(expected, actual, skip_details=True) def test_compare_directory_failure_different_listing() -> None: expected = { "stuff": { "class": "Directory", "listing": [ { "basename": "baz.txt", "checksum": "sha1$da39a3ee5e6b4b0d3255bfef95601890afd80709", "class": "File", "size": 0, }, { "basename": "foo", "class": "Directory", "listing": [ { "basename": "bar.txt", "checksum": "sha1$da39a3ee5e6b4b0d3255bfef95601890afd80709", "class": "File", "size": 0, } ], }, ], } } actual = { "stuff": { "class": "Directory", "location": "file:///var/folders/8x/2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/stuff", "path": "/var/folders/8x/2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/stuff", "listing": [ { "basename": "baz.txt", "checksum": "sha1$da39a3ee5e6b4b0d3255bfef95601890afd80709", "class": "File", "location": "file:///var/folders/8x/" "2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/stuff/baz.txt", "path": "/var/folders/8x/2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/stuff/baz.txt", "size": 0, }, { "basename": "foo", "class": "Directory", "location": "file:///var/folders/8x/" "2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/stuff/foo", "path": "/var/folders/8x/2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/stuff/foo", "listing": [ { "basename": "bar.txt", "checksum": "sha1$da39a3ee5e6b4b0d3255bfef95601890afd80775", "class": "File", "location": "file:///var/folders/8x/" "2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/stuff/foo/bar.txt", "path": "/var/folders/8x/" "2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/stuff/foo/bar.txt", "size": 0, } ], }, ], } } with pytest.raises(CompareFail): compare(expected, actual) def test_compare_directory_failure_no_listing() -> None: expected = { "stuff": { "class": "Directory", "listing": [ { "basename": "baz.txt", "checksum": "sha1$da39a3ee5e6b4b0d3255bfef95601890afd80709", "class": "File", "size": 0, }, { "basename": "foo", "class": "Directory", "listing": [ { "basename": "bar.txt", "checksum": "sha1$da39a3ee5e6b4b0d3255bfef95601890afd80709", "class": "File", "size": 0, } ], }, ], } } actual = { "stuff": { "class": "Directory", "location": "file:///var/folders/8x/2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/stuff", "path": "/var/folders/8x/2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/stuff", "listing": [ { "basename": "baz.txt", "checksum": "sha1$da39a3ee5e6b4b0d3255bfef95601890afd80709", "class": "File", "location": "file:///var/folders/8x/" "2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/stuff/baz.txt", "path": "/var/folders/8x/2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/stuff/baz.txt", "size": 0, }, { "basename": "foo", "class": "Directory", "location": "file:///var/folders/8x/2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/stuff/foo", "path": "/var/folders/8x/2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/stuff/foo", }, ], } } with pytest.raises(CompareFail): compare(expected, actual) def test_compare_file_failure_different() -> None: expected = { "location": "cores.txt", "size": 2, "class": "File", "checksum": "sha1$7448d8798a4380162d4b56f9b452e2f6f9e24e7b", } actual = { "basename": "cores.txt", "checksum": "sha1$7448d8798a4380162d4b56f9b452e2f6f9e24e7a", "class": "File", "location": "file:///var/folders/8x/2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/cores.txt", "path": "/var/folders/8x/2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/cores.txt", "size": 2, } with pytest.raises(CompareFail): compare(expected, actual) def test_compare_file_failure_none() -> None: expected = { "location": "cores.txt", "size": 2, "class": "File", "checksum": "sha1$7448d8798a4380162d4b56f9b452e2f6f9e24e7b", } actual: dict[str, Any] = {} with pytest.raises(CompareFail): compare(expected, actual) def test_compare_file_success() -> None: expected = { "location": "cores.txt", "size": 2, "class": "File", "checksum": "sha1$7448d8798a4380162d4b56f9b452e2f6f9e24e7a", } actual = { "basename": "cores.txt", "checksum": "sha1$7448d8798a4380162d4b56f9b452e2f6f9e24e7a", "class": "File", "location": "file:///var/folders/8x/2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/cores.txt", "path": "/var/folders/8x/2df05_7j20j6r8y81w4qf43r0000gn/T/tmpG0EkrS/cores.txt", "size": 2, } compare(expected, actual, skip_details=True) def test_compare_list_failure_missing() -> None: expected = { "args": [ "tmap", "mapall", "stage1", "map1", "--min-seq-length", "20", "map2", "--min-seq-length", "20", "stage2", "map1", "--max-seq-length", "20", "--min-seq-length", "10", "--seed-length", "16", "map2", "--max-seed-hits", "-1", "--max-seq-length", "20", "--min-seq-length", "10", ] } actual = { "args": [ "tmap", "mapall", "stage1", "map1", "stage2", "map1", "--max-seq-length", "20", "--min-seq-length", "10", "--seed-length", "16", "map2", "--max-seed-hits", "-1", "--max-seq-length", "20", "--min-seq-length", "10", ] } with pytest.raises(CompareFail): compare(expected, actual) def test_compare_list_failure_order() -> None: expected = { "args": [ "tmap", "mapall", "stage1", "map1", "--min-seq-length", "20", "map2", "--min-seq-length", "20", "stage2", "map1", "--max-seq-length", "20", "--min-seq-length", "10", "--seed-length", "16", "map2", "--max-seed-hits", "-1", "--max-seq-length", "20", "--min-seq-length", "10", ] } actual = { "args": [ "tmap", "mapall", "stage1", "map1", "--min-seq-length", "20", "map2", "--min-seq-length", "20", "stage2", "map2", "--max-seed-hits", "-1", "--max-seq-length", "20", "--min-seq-length", "10", "map1", "--max-seq-length", "20", "--min-seq-length", "10", "--seed-length", "16", ] } with pytest.raises(CompareFail): compare(expected, actual) def test_compare_list_failure_type() -> None: expected = { "args": [ "tmap", "mapall", "stage1", "map1", "--min-seq-length", "20", "map2", "--min-seq-length", "20", "stage2", "map1", "--max-seq-length", "20", "--min-seq-length", "10", "--seed-length", "16", "map2", "--max-seed-hits", "-1", "--max-seq-length", "20", "--min-seq-length", "10", ] } actual: dict[str, Any] = {"args": {}} with pytest.raises(CompareFail): compare(expected, actual) def test_compare_list_success() -> None: expected = { "args": [ "tmap", "mapall", "stage1", "map1", "--min-seq-length", "20", "map2", "--min-seq-length", "20", "stage2", "map1", "--max-seq-length", "20", "--min-seq-length", "10", "--seed-length", "16", "map2", "--max-seed-hits", "-1", "--max-seq-length", "20", "--min-seq-length", "10", ] } compare(expected, expected) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test_exclude_tags.py0000664000175000017500000000156315273371155021520 0ustar00michaelmichaelfrom os import linesep as n from .util import get_data, run_with_mock_cwl_runner def test_list_only_exclude() -> None: args = [ "--test", get_data("tests/test-data/exclude-tags.yml"), "-l", "--exclude-tags=workflow", ] error_code, stdout, stderr = run_with_mock_cwl_runner(args) assert f"[1] opt-error1: Test with label{n}" in stdout assert "opt-error2" not in stdout assert "opt-error3" not in stdout def test_list_include_and_exclude() -> None: args = [ "--test", get_data("tests/test-data/exclude-tags.yml"), "-l", "--tags=command_line_tool", "--exclude-tags=workflow", ] error_code, stdout, stderr = run_with_mock_cwl_runner(args) assert f"[1] opt-error1: Test with label{n}" in stdout assert "opt-error2" not in stdout assert "opt-error3" not in stdout ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test_integer_id.py0000664000175000017500000000060315273371155021154 0ustar00michaelmichaelfrom .util import get_data, run_with_mock_cwl_runner def test_warning_with_integer_id() -> None: args = [ "--test", get_data("tests/test-data/integer-id.yml"), "-l", ] error_code, stdout, stderr = run_with_mock_cwl_runner(args) assert ( "The `id` field with integer is deprecated. Use string identifier instead." in stderr ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test_invalid_outputs.py0000664000175000017500000000100115273371155022265 0ustar00michaelmichaelfrom pathlib import Path import schema_salad.ref_resolver from .util import get_data, run_with_mock_cwl_runner def test_invalid_outputs(tmp_path: Path) -> None: args = [ "--test", schema_salad.ref_resolver.file_uri(get_data("tests/test-data/nothing.yml")), ] error_code, stdout, stderr = run_with_mock_cwl_runner( args, get_data("tests/test-data/dummy-executor.sh") ) assert error_code == 1 assert "0 tests passed, 2 failures, 0 unsupported features" in stderr ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test_load_custom_fsaccess.py0000664000175000017500000000323015273371155023225 0ustar00michaelmichaelfrom typing import Callable from unittest.mock import MagicMock, patch, sentinel import pytest from cwltest.utils import load_optional_fsaccess_plugin def get_mock_entry_point(set_sentinel: bool = False) -> MagicMock: """Returns a mock object whose `load()` method may optionally return a sentinel value. """ m: MagicMock = MagicMock() if set_sentinel: m.load.return_value.return_value = sentinel.custom_fs_access return m def mock_entry_points(n: int = 1) -> Callable[..., MagicMock]: """Returns a plain function that emulates `importlib.metadata.entry_points`. """ mock_ep_data: tuple[MagicMock, ...] = tuple( get_mock_entry_point(i == 0) for i in range(n) ) mock_eps: MagicMock = MagicMock() mock_eps.__iter__ = lambda self: iter(mock_ep_data) mock_eps.__len__.return_value = n mock_eps.__getitem__.side_effect = KeyError def entry_points(**kwargs: str) -> MagicMock: return mock_eps return entry_points def test_default_fsaccess_fallback() -> None: with patch("cwltest.utils.entry_points", mock_entry_points(0)): load_optional_fsaccess_plugin() from cwltest import compare, stdfsaccess assert compare.fs_access assert isinstance(compare.fs_access, stdfsaccess.StdFsAccess) @pytest.mark.parametrize("n", (1, 2)) def test_load_fsaccess(n: int) -> None: with patch("cwltest.utils.entry_points", mock_entry_points(n)) as mfcn: load_optional_fsaccess_plugin() ep = tuple(mfcn())[0] ep.load.assert_called_once_with() import cwltest.compare assert cwltest.compare.fs_access is sentinel.custom_fs_access ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test_multi_lined_doc.py0000664000175000017500000000132115273371155022173 0ustar00michaelmichaelfrom os import linesep as n from .util import get_data, run_with_mock_cwl_runner def test_run() -> None: args = ["--test", get_data("tests/test-data/multi-lined-doc.yml")] error_code, stdout, stderr = run_with_mock_cwl_runner(args) assert "The `label` field is deprecated. Use `id` field instead." in stderr assert f"Test [1/2] opt-error: Test with label{n}" in stderr assert f"Test [2/2] Test without label{n}" in stderr def test_list() -> None: args = ["--test", get_data("tests/test-data/multi-lined-doc.yml"), "-l"] error_code, stdout, stderr = run_with_mock_cwl_runner(args) assert f"[1] opt-error: Test with label{n}" in stdout assert f"[2] Test without label{n}" in stdout ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test_numeric_id.py0000664000175000017500000000065515273371155021170 0ustar00michaelmichaelfrom 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test_plugin.py0000664000175000017500000001365415273371155020353 0ustar00michaelmichaelimport os import shutil from pathlib import Path import pytest from .util import get_data def _load_v1_0_dir(path: Path) -> None: inner_dir = os.path.join(path.parent, "v1.0") os.mkdir(inner_dir) shutil.copy(get_data("tests/test-data/v1.0/cat1-testcli.cwl"), inner_dir) shutil.copy(get_data("tests/test-data/v1.0/null-expression2-tool.cwl"), inner_dir) shutil.copy(get_data("tests/test-data/v1.0/cat-job.json"), inner_dir) shutil.copy(get_data("tests/test-data/v1.0/cat-n-job.json"), inner_dir) shutil.copy(get_data("tests/test-data/v1.0/hello.txt"), inner_dir) shutil.copy(get_data("tests/test-data/v1.0/args.py"), inner_dir) 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( get_data("tests/test-data/cwltool-conftest.py"), path.parent / "conftest.py" ) _load_v1_0_dir(path) result = pytester.runpytest( "-k", "conformance_test_v1.0.cwltest.yml", "--cwl-include", "cl_optional_inputs_missing", ) result.assert_outcomes(passed=1, skipped=1) 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( get_data("tests/test-data/cwltool-conftest.py"), path.parent / "conftest.py" ) _load_v1_0_dir(path) result = pytester.runpytest( "-k", "conformance_test_v1.0.cwltest.yml", "--cwl-exclude", "cl_optional_inputs_missing,cl_optional_bindings_provided", ) result.assert_outcomes(passed=0, skipped=2) 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( get_data("tests/test-data/cwltool-conftest.py"), path.parent / "conftest.py" ) _load_v1_0_dir(path) result = pytester.runpytest( "-k", "conformance_test_v1.0.cwltest.yml", "--cwl-tags", "required" ) result.assert_outcomes(passed=1, skipped=1) 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( get_data("tests/test-data/cwltool-conftest.py"), path.parent / "conftest.py" ) _load_v1_0_dir(path) result = pytester.runpytest( "-k", "conformance_test_v1.0.cwltest.yml", "--cwl-exclude-tags", "command_line_tool", ) result.assert_outcomes(skipped=2) 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) 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), ) 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: 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( get_data("tests/test-data/cwltool-conftest.py"), path.parent / "conftest.py" ) _load_v1_0_dir(path) result = pytester.runpytest( "-k", "conformance_test_v1.2.cwltest.yaml", "--cwl-tags", "required" ) result.assert_outcomes(passed=2, skipped=1) 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( get_data("tests/test-data/cwltool-conftest.py"), path.parent / "conftest.py" ) _load_v1_0_dir(path) result = pytester.runpytest("-k", "conformance_test_v1.0.cwltest.yml") result.assert_outcomes(passed=2) 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) result = pytester.runpytest("-k", "conformance_test_v1.0.cwltest.yml") result.assert_outcomes(passed=2) def test_integer_id(pytester: pytest.Pytester) -> None: """Test that the pytest plugin supports integer ids.""" path = pytester.copy_example("integer-id.cwltest.yml") _load_v1_0_dir(path) result = pytester.runpytest("-k", "integer-id.cwltest.yml") result.assert_outcomes(passed=1) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test_prepare.py0000664000175000017500000000237015273371155020504 0ustar00michaelmichael"""Test prepare_test_command()""" import os from pathlib import Path import pytest from cwltest import utils @pytest.mark.parametrize("use_outdir", [False, True]) def test_unix_relative_path( use_outdir: bool, tmp_path: Path, ) -> None: """Confirm unix style to windows style path corrections.""" basedir = tmp_path outdir = str(basedir) if use_outdir else None command, test_outdir = utils.prepare_test_command( tool="cwl-runner", args=[], testargs=None, test={ "doc": "General test of command line generation", "output": {"args": ["echo"]}, "tool": "v1.0/bwa-mem-tool.cwl", "job": "v1.0/bwa-mem-job.json", "tags": ["required"], }, cwd=os.getcwd(), outdir=outdir, ) if os.name == "nt": assert command[3] == r"v1.0\bwa-mem-tool.cwl" assert command[4] == r"v1.0\bwa-mem-job.json" else: assert command[3] == "v1.0/bwa-mem-tool.cwl" assert command[4] == "v1.0/bwa-mem-job.json" assert test_outdir is not None assert Path(test_outdir).is_dir() if use_outdir: assert Path(test_outdir).parent == basedir else: assert Path(test_outdir).parent != basedir ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test_short_names.py0000664000175000017500000000322715273371155021372 0ustar00michaelmichaelfrom os import linesep as n from pathlib import Path import defusedxml.ElementTree as ET from .util import get_data, run_with_mock_cwl_runner def test_stderr_output() -> None: args = ["--test", get_data("tests/test-data/short-names.yml")] error_code, stdout, stderr = run_with_mock_cwl_runner(args) assert f"Test [1/1] opt-error: Test with a short name{n}" in stderr def test_run_by_short_name() -> None: short_name = "opt-error" args = [ "--test", get_data("tests/test-data/with-and-without-short-names.yml"), "-s", short_name, ] error_code, stdout, stderr = run_with_mock_cwl_runner(args) assert "Test [2/2] opt-error: Test with a short name" in stderr assert "Test [1/2]" not in stderr def test_list_tests() -> None: args = [ "--test", get_data("tests/test-data/with-and-without-short-names.yml"), "-l", ] error_code, stdout, stderr = run_with_mock_cwl_runner(args) assert ( f"[1] Test without a short name{n}" f"[2] opt-error: Test with a short name{n}" ) in stdout def test_short_name_in_junit_xml(tmp_path: Path) -> None: junit_xml_report = tmp_path / "junit-report.xml" args = [ "--test", get_data("tests/test-data/short-names.yml"), "--junit-xml", str(junit_xml_report), ] run_with_mock_cwl_runner(args) tree = ET.parse(junit_xml_report) 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" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test_string_id.py0000664000175000017500000000047415273371155021033 0ustar00michaelmichaelfrom os import linesep as n from .util import get_data, run_with_mock_cwl_runner def test_list() -> None: args = ["--test", get_data("tests/test-data/string-id.yml"), "-l"] error_code, stdout, stderr = run_with_mock_cwl_runner(args) assert f"[1] test-string-id: Test with a string label{n}" in stdout ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/test_timeout.py0000664000175000017500000000265415273371155020541 0ustar00michaelmichaelimport os from pathlib import Path import defusedxml.ElementTree as ET import schema_salad.ref_resolver from .util import get_data, run_with_mock_cwl_runner def test_timeout_stderr_stdout(tmp_path: Path) -> None: junit_xml_report = tmp_path / "junit-report.xml" args = [ "--test", schema_salad.ref_resolver.file_uri(get_data("tests/test-data/timeout.yml")), "--timeout", "5", "--junit-xml", str(junit_xml_report), ] cwd = os.getcwd() try: os.chdir(get_data("tests/test-data/")) error_code, stdout, stderr = run_with_mock_cwl_runner(args) finally: os.chdir(cwd) assert error_code == 1 assert "Test 1 timed out" in stderr tree = ET.parse(junit_xml_report) try: 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()) raise e ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tests/util.py0000664000175000017500000000254315273371155016766 0ustar00michaelmichael"""Test functions.""" import atexit import os import subprocess # nosec from contextlib import ExitStack from importlib.resources import as_file, files from pathlib import Path def get_data(filename: str) -> str: """Return the absolute path starting from a file name.""" filename = os.path.normpath(filename) # normalizing path depending on OS or else it will cause problem when # joining path filepath = None try: file_manager = ExitStack() atexit.register(file_manager.close) traversable = files("cwltest") / filename filepath = file_manager.enter_context(as_file(traversable)) except ModuleNotFoundError: pass if not filepath or not os.path.isfile(filepath): filepath = Path(os.path.dirname(__file__)) / ".." / filename return str(filepath.resolve()) def run_with_mock_cwl_runner( args: list[str], cwl_runner: str | None = 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") process = subprocess.Popen( # nosec ["cwltest", "--tool", cwl_runner] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) stdout, stderr = process.communicate() return process.returncode, stdout.decode(), stderr.decode() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1793978989.3968244 cwltest-2.7.20261106145043/tox.ini0000664000175000017500000000436515273371155015614 0ustar00michaelmichael[tox] envlist = py3{10,11,12,13,14,15}-lint, py3{10,11,12,13,14,15}-unit, py3{10,11,12,13,14,15}-bandit, py3{10,11,12,13,14,15}-mypy, py314-lintreadme, py314-pydocstyle skip_missing_interpreters = True isolated_build = True [pytest] addopts = -p pytester -n auto pytester_example_dir = tests/test-data testpaths = tests [gh-actions] python = 3.10: py310 3.11: py311 3.12: py312 3.13: py313 3.14: py314 3.15: py315 [testenv] skipsdist = py3{10,11,12,13,14,15}-!{unit,mypy,lintreadme} = True description = py3{10,11,12,13,14,15}-unit: Run the unit tests py3{10,11,12,13,14,15}-lint: Lint the Python code py3{10,11,12,13,14,15}-bandit: Search for common security issues py3{10,11,12,13,14,15}-mypy: Check for type safety py314-pydocstyle: docstring style checker py314-lintreadme: Lint the README.rst->.md conversion passenv = CI GITHUB_* dependency_groups = py3{10,11,12,13,14,15}-{unit,mypy}: test py3{10,11,12,13,14,15}-lint: lint py3{10,11,12,13,14,15}-mypy: mypy deps = py3{10,11,12,13,14,15}-bandit: bandit set_env = py3{10,11,12,13,14,15}-unit: LC_ALL = C.UTF-8 COV_CORE_SOURCE=cwltest COV_CORE_CONFIG={toxinidir}/.coveragerc COV_CORE_DATAFILE={toxinidir}/.coverage.eager commands = py3{10,11,12,13,14,15}-unit: python -m pip install -U pip setuptools wheel py3{10,11,12,13,14,15}-unit: python -m pytest --cov --cov-config={toxinidir}/.coveragerc --cov-append {posargs} py3{10,11,12,13,14,15}-unit: coverage xml py3{10,11,12,13,14,15}-bandit: bandit --recursive src py3{10,11,12,13,14,15}-lint: make flake8 py3{10,11,12,13,14,15}-lint: make format-check py3{10,11,12,13,14,15}-mypy: make mypy allowlist_externals = py3{10,11,12,13,14,15}-lint: flake8 py3{10,11,12,13,14,15}-lint: black py3{10,11,12,13,14,15}-{mypy,shellcheck,lint,unit}: make skip_install = py3{10,11,12,13,14,15}-lint: true py3{10,11,12,13,14,15}-bandit: true [testenv:py314-pydocstyle] allowlist_externals = make commands = make diff_pydocstyle_report deps = pydocstyle diff-cover skip_install = true [testenv:py314-lintreadme] description = Lint the README.rst->.md conversion commands = python -m build --outdir dist twine check dist/* deps = twine build readme_renderer[rst] skip_install = true