Skip to content

Commit af433f7

Browse files
mr-cGlassOfWhiskey
andauthored
cwltest plugin for pytest (#74)
Co-authored-by: GlassOfWhiskey <[email protected]>
1 parent b80be60 commit af433f7

34 files changed

+1986
-523
lines changed

.coveragerc

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
1+
[paths]
2+
source =
3+
cwltest
4+
*/site-packages/cwltest
5+
*/cwltest
6+
17
[run]
28
branch = True
3-
source = cwltest
4-
omit = tests/*
9+
source_pkgs = cwltest
10+
omit =
11+
tests/*
12+
*/site-packages/cwltest/tests/*
513

614
[report]
715
exclude_lines =
816
if self.debug:
917
pragma: no cover
1018
raise NotImplementedError
1119
if __name__ == .__main__.:
20+
if TYPE_CHECKING:
21+
\.\.\.
1222
ignore_errors = True
13-
omit = tests/*
23+
omit =
24+
tests/*
25+
*/site-packages/cwltest/tests/*

Makefile

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ docs: FORCE
7676
clean: FORCE
7777
rm -f ${MODILE}/*.pyc tests/*.pyc
7878
python setup.py clean --all || true
79-
rm -Rf .coverage
79+
rm -Rf .coverage\.* .coverage
8080
rm -f diff-cover.html
8181

8282
# Linting and code style related targets
@@ -104,11 +104,11 @@ codespell:
104104
codespell -w $(shell git ls-files | grep -v mypy-stubs | grep -v gitignore)
105105

106106
## format : check/fix all code indentation and formatting (runs black)
107-
format:
108-
black setup.py setup.py cwltest tests mypy-stubs
107+
format: $(PYSOURCES) mypy-stubs
108+
black $^
109109

110-
format-check:
111-
black --diff --check setup.py cwltest tests mypy-stubs
110+
format-check: $(PYSOURCES) mypy-stubs
111+
black --diff --check $^
112112

113113
## pylint : run static code analysis on Python code
114114
pylint: $(PYSOURCES)
@@ -123,7 +123,9 @@ diff_pylint_report: pylint_report.txt
123123
diff-quality --compare-branch=main --violations=pylint pylint_report.txt
124124

125125
.coverage: $(PYSOURCES)
126-
python setup.py test --addopts "--cov --cov-config=.coveragerc --cov-report= ${PYTEST_EXTRA}"
126+
COV_CORE_SOURCE=cwltest COV_CORE_CONFIG=.coveragerc COV_CORE_DATAFILE=.coverage \
127+
python -m pytest --cov --cov-append --cov-report=
128+
# https://pytest-cov.readthedocs.io/en/latest/plugins.html#plugin-coverage
127129

128130
coverage.xml: .coverage
129131
coverage xml

README.rst

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
==========================================
1+
##########################################
22
Common Workflow Language testing framework
3-
==========================================
3+
##########################################
44

55
|Linux Build Status| |Code coverage|
66

@@ -34,8 +34,12 @@ conformance tests.
3434

3535
This is written and tested for Python 3.6, 3.7, 3.8, 3.9, 3.10, and 3.11.
3636

37+
.. contents:: Table of Contents
38+
:local:
39+
40+
*******
3741
Install
38-
-------
42+
*******
3943

4044
Installing the official package from PyPi
4145

@@ -56,15 +60,17 @@ Or from source
5660
git clone https://github.com/common-workflow-language/cwltest.git
5761
cd cwltest && python setup.py install
5862
63+
***********************
5964
Run on the command line
60-
-----------------------
65+
***********************
6166

6267
Simple command::
6368

6469
cwltest --test test-descriptions.yml --tool cwl-runner
6570

71+
*****************************************
6672
Generate conformance badges using cwltest
67-
-----------------------------------------
73+
*****************************************
6874

6975
To make badges that show the results of the conformance test,
7076
you can generate JSON files for https://badgen.net by using --badgedir option

cwltest/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Common Workflow Language testing framework."""
1+
"""Run CWL descriptions with a cwl-runner, and look for expected output."""
22

33
import logging
44
import threading

cwltest/argparser.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from cwltest import DEFAULT_TIMEOUT
88

99

10-
def arg_parser(): # type: () -> argparse.ArgumentParser
10+
def arg_parser() -> argparse.ArgumentParser:
1111
"""Generate a command Line argument parser for cwltest."""
1212
parser = argparse.ArgumentParser(
1313
description="Common Workflow Language testing framework"
@@ -60,7 +60,8 @@ def arg_parser(): # type: () -> argparse.ArgumentParser
6060
parser.add_argument(
6161
"--junit-verbose",
6262
action="store_true",
63-
help="Store more verbose output to JUnit xml file",
63+
help="Store more verbose output to JUnit XML file by not passing "
64+
"'--quiet' to the CWL runner.",
6465
)
6566
parser.add_argument(
6667
"--test-arg",
@@ -101,7 +102,9 @@ def arg_parser(): # type: () -> argparse.ArgumentParser
101102
),
102103
)
103104
parser.add_argument(
104-
"--badgedir", type=str, help="Directory that stores JSON files for badges."
105+
"--badgedir",
106+
type=str,
107+
help="Create JSON badges and store them in this directory.",
105108
)
106109

107110
pkg = pkg_resources.require("cwltest")

cwltest/compare.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
"""Compare utilities for CWL objects."""
2+
3+
import json
4+
from typing import Any, Dict, Set
5+
6+
7+
class CompareFail(Exception):
8+
"""Compared CWL objects are not equal."""
9+
10+
@classmethod
11+
def format(cls, expected, actual, cause=None):
12+
# type: (Any, Any, Any) -> CompareFail
13+
"""Load the difference details into the error message."""
14+
message = "expected: {}\ngot: {}".format(
15+
json.dumps(expected, indent=4, sort_keys=True),
16+
json.dumps(actual, indent=4, sort_keys=True),
17+
)
18+
if cause:
19+
message += "\ncaused by: %s" % cause
20+
return cls(message)
21+
22+
23+
def _check_keys(keys, expected, actual):
24+
# type: (Set[str], Dict[str,Any], Dict[str,Any]) -> None
25+
for k in keys:
26+
try:
27+
compare(expected.get(k), actual.get(k))
28+
except CompareFail as e:
29+
raise CompareFail.format(
30+
expected, actual, f"field '{k}' failed comparison: {str(e)}"
31+
) from e
32+
33+
34+
def _compare_contents(expected, actual):
35+
# type: (Dict[str,Any], Dict[str,Any]) -> None
36+
expected_contents = expected["contents"]
37+
with open(actual["path"]) as f:
38+
actual_contents = f.read()
39+
if expected_contents != actual_contents:
40+
raise CompareFail.format(
41+
expected,
42+
actual,
43+
json.dumps(
44+
"Output file contents do not match: actual '%s' is not equal to expected '%s'"
45+
% (actual_contents, expected_contents)
46+
),
47+
)
48+
49+
50+
def _compare_dict(expected, actual):
51+
# type: (Dict[str,Any], Dict[str,Any]) -> None
52+
for c in expected:
53+
try:
54+
compare(expected[c], actual.get(c))
55+
except CompareFail as e:
56+
raise CompareFail.format(
57+
expected, actual, f"failed comparison for key '{c}': {e}"
58+
) from e
59+
extra_keys = set(actual.keys()).difference(list(expected.keys()))
60+
for k in extra_keys:
61+
if actual[k] is not None:
62+
raise CompareFail.format(expected, actual, "unexpected key '%s'" % k)
63+
64+
65+
def _compare_directory(expected, actual):
66+
# type: (Dict[str,Any], Dict[str,Any]) -> None
67+
if actual.get("class") != "Directory":
68+
raise CompareFail.format(
69+
expected, actual, "expected object with a class 'Directory'"
70+
)
71+
if "listing" not in actual:
72+
raise CompareFail.format(
73+
expected, actual, "'listing' is mandatory field in Directory object"
74+
)
75+
for i in expected["listing"]:
76+
found = False
77+
for j in actual["listing"]:
78+
try:
79+
compare(i, j)
80+
found = True
81+
break
82+
except CompareFail:
83+
pass
84+
if not found:
85+
raise CompareFail.format(
86+
expected,
87+
actual,
88+
"%s not found" % json.dumps(i, indent=4, sort_keys=True),
89+
)
90+
_compare_file(expected, actual)
91+
92+
93+
def _compare_file(expected, actual):
94+
# type: (Dict[str,Any], Dict[str,Any]) -> None
95+
_compare_location(expected, actual)
96+
if "contents" in expected:
97+
_compare_contents(expected, actual)
98+
other_keys = set(expected.keys()) - {"path", "location", "listing", "contents"}
99+
_check_keys(other_keys, expected, actual)
100+
_check_keys(other_keys, expected, actual)
101+
102+
103+
def _compare_location(expected, actual):
104+
# type: (Dict[str,Any], Dict[str,Any]) -> None
105+
if "path" in expected:
106+
comp = "path"
107+
if "path" not in actual:
108+
actual["path"] = actual["location"]
109+
elif "location" in expected:
110+
comp = "location"
111+
else:
112+
return
113+
if actual.get("class") == "Directory":
114+
actual[comp] = actual[comp].rstrip("/")
115+
116+
if expected[comp] != "Any" and (
117+
not (
118+
actual[comp].endswith("/" + expected[comp])
119+
or ("/" not in actual[comp] and expected[comp] == actual[comp])
120+
)
121+
):
122+
raise CompareFail.format(
123+
expected,
124+
actual,
125+
f"{actual[comp]} does not end with {expected[comp]}",
126+
)
127+
128+
129+
def compare(expected, actual): # type: (Any, Any) -> None
130+
"""Compare two CWL objects."""
131+
if expected == "Any":
132+
return
133+
if expected is not None and actual is None:
134+
raise CompareFail.format(expected, actual)
135+
136+
try:
137+
if isinstance(expected, dict):
138+
if not isinstance(actual, dict):
139+
raise CompareFail.format(expected, actual)
140+
141+
if expected.get("class") == "File":
142+
_compare_file(expected, actual)
143+
elif expected.get("class") == "Directory":
144+
_compare_directory(expected, actual)
145+
else:
146+
_compare_dict(expected, actual)
147+
148+
elif isinstance(expected, list):
149+
if not isinstance(actual, list):
150+
raise CompareFail.format(expected, actual)
151+
152+
if len(expected) != len(actual):
153+
raise CompareFail.format(expected, actual, "lengths don't match")
154+
for c in range(0, len(expected)):
155+
try:
156+
compare(expected[c], actual[c])
157+
except CompareFail as e:
158+
raise CompareFail.format(expected, actual, e) from e
159+
else:
160+
if expected != actual:
161+
raise CompareFail.format(expected, actual)
162+
163+
except Exception as e:
164+
raise CompareFail(str(e)) from e

cwltest/hooks.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""Hooks for pytest-cwl users."""
2+
3+
from typing import Any, Dict, Optional, Tuple
4+
5+
from cwltest import utils
6+
7+
8+
def pytest_cwl_execute_test( # type: ignore[empty-body]
9+
config: utils.CWLTestConfig, processfile: str, jobfile: Optional[str]
10+
) -> Tuple[int, Optional[Dict[str, Any]]]:
11+
"""
12+
Execute CWL test using a Python function instead of a command line runner.
13+
14+
The return value is a tuple.
15+
- status code
16+
- 0 = success
17+
- :py:attr:`cwltest.UNSUPPORTED_FEATURE` for an unsupported feature
18+
- and any other number for failure
19+
- CWL output object using plain Python objects.
20+
21+
:param processfile: a path to a CWL document
22+
:param jobfile: an optionl path to JSON/YAML input object
23+
"""

0 commit comments

Comments
 (0)