Skip to content

Commit 775ec14

Browse files
committed
implement basic checks; use ECMAScript for regex
1 parent 3d9cbca commit 775ec14

File tree

4 files changed

+162
-7
lines changed

4 files changed

+162
-7
lines changed

cwltool/extensions-v1.2.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,7 @@ $graph:
199199
- name: regex
200200
type: record
201201
doc: |
202-
Regular Expression
203-
TODO: what type of regex?
202+
ECMAScript 5.1 Regular Expression constraint.
204203
fields:
205204
class:
206205
type:
@@ -213,7 +212,13 @@ $graph:
213212
_type: "@vocab"
214213
rpattern:
215214
type: string
216-
215+
doc: |
216+
Testing should be the equivalent of calling `/rpattern/.test(value)`
217+
where `rpattern` is the value of the `rpattern` field, and `value`
218+
is the input object value to be tested. If the result is `true` then the
219+
input object value is accepted. If the result if `false` then the
220+
input object value does not match this constraint.
221+
<https://262.ecma-international.org/5.1/#sec-15.10.6.3>
217222
218223
- name: Restrictions
219224
type: record

cwltool/process.py

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
Iterable,
2121
Iterator,
2222
List,
23+
Mapping,
2324
MutableMapping,
2425
MutableSequence,
2526
Optional,
@@ -55,6 +56,7 @@
5556
from .loghandler import _logger
5657
from .mpi import MPIRequirementName
5758
from .pathmapper import MapperEnt, PathMapper
59+
from .sandboxjs import execjs
5860
from .secrets import SecretStore
5961
from .stdfsaccess import StdFsAccess
6062
from .update import INTERNAL_VERSION, ORIGINAL_CWLVERSION
@@ -523,10 +525,11 @@ def var_spool_cwl_detector(
523525
r = False
524526
if isinstance(obj, str):
525527
if "var/spool/cwl" in obj and obj_key != "dockerOutputDirectory":
528+
debug = _logger.isEnabledFor(logging.DEBUG)
526529
_logger.warning(
527-
SourceLine(item=item, key=obj_key, raise_type=str).makeError(
528-
_VAR_SPOOL_ERROR.format(obj)
529-
)
530+
SourceLine(
531+
item=item, key=obj_key, raise_type=str, include_traceback=debug
532+
).makeError(_VAR_SPOOL_ERROR.format(obj))
530533
)
531534
r = True
532535
elif isinstance(obj, MutableMapping):
@@ -743,7 +746,9 @@ def __init__(
743746
and not is_req
744747
):
745748
_logger.warning(
746-
SourceLine(item=dockerReq, raise_type=str).makeError(
749+
SourceLine(
750+
item=dockerReq, raise_type=str, include_traceback=debug
751+
).makeError(
747752
"When 'dockerOutputDirectory' is declared, DockerRequirement "
748753
"should go in the 'requirements' section, not 'hints'."
749754
""
@@ -805,6 +810,98 @@ def _init_job(
805810
vocab=INPUT_OBJ_VOCAB,
806811
)
807812

813+
restriction_req, mandatory_restrictions = self.get_requirement(
814+
"ParameterRestrictions"
815+
)
816+
817+
if restriction_req:
818+
restrictions = restriction_req["restrictions"]
819+
for entry in cast(List[CWLObjectType], restrictions):
820+
name = shortname(cast(str, entry["input"]))
821+
if name in job:
822+
value = job[name]
823+
matched = False
824+
for constraint in cast(
825+
List[CWLOutputType], entry["constraints"]
826+
):
827+
if isinstance(constraint, Mapping):
828+
if constraint["class"] == "intInterval":
829+
if not isinstance(value, int):
830+
raise SourceLine(
831+
constraint,
832+
None,
833+
WorkflowException,
834+
runtime_context.debug,
835+
).makeError(
836+
"intInterval parameter restriction is only valid for inputs of type 'int'; "
837+
f"instead got {type(value)}: {value}."
838+
)
839+
low = cast(
840+
Union[int, float],
841+
constraint.get("low", -math.inf),
842+
)
843+
high = cast(
844+
Union[int, float],
845+
constraint.get("high", math.inf),
846+
)
847+
matched = value >= low and value <= high
848+
elif constraint["class"] == "realInterval":
849+
if not isinstance(value, (int, float)):
850+
raise SourceLine(
851+
constraint,
852+
None,
853+
WorkflowException,
854+
runtime_context.debug,
855+
).makeError(
856+
"realInterval parameter restriction is only valid for inputs of type 'int', 'float', and 'double'; "
857+
f"instead got {type(value)}: {value}."
858+
)
859+
low = cast(
860+
Union[int, float],
861+
constraint.get("low", -math.inf),
862+
)
863+
high = cast(
864+
Union[int, float],
865+
constraint.get("high", math.inf),
866+
)
867+
low_inclusive = constraint.get(
868+
"low_inclusive", True
869+
)
870+
high_inclusive = constraint.get(
871+
"high_inclusive", True
872+
)
873+
check_low = (
874+
value >= low if low_inclusive else value > low
875+
)
876+
check_high = (
877+
value <= high if low_inclusive else value < high
878+
)
879+
matched = check_low and check_high
880+
elif constraint["class"] == "regex":
881+
rpattern = constraint["rpattern"]
882+
quoted_value = json.dumps(value)
883+
matched = cast(
884+
bool,
885+
execjs(
886+
f"/{rpattern}/.test({quoted_value})",
887+
"",
888+
runtime_context.eval_timeout,
889+
runtime_context.force_docker_pull,
890+
),
891+
)
892+
elif constraint == value:
893+
matched = True
894+
if matched:
895+
break
896+
if not matched:
897+
raise SourceLine(
898+
job, name, WorkflowException, runtime_context.debug
899+
).makeError(
900+
f"The field '{name}' is not valid because its "
901+
f"value '{value}' failed to match any of the "
902+
f"constraints '{json.dumps(entry['constraints'])}'."
903+
)
904+
808905
if load_listing and load_listing != "no_listing":
809906
get_listing(fs_access, job, recursive=(load_listing == "deep_listing"))
810907

tests/parameter_restrictions.cwl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ doc: |
77
inputs:
88
one: double
99
two: string
10+
three: int
1011

1112

1213
hints:
@@ -24,6 +25,10 @@ hints:
2425
two:
2526
- class: cwltool:regex
2627
rpattern: "foo.*bar"
28+
three:
29+
- class: cwltool:intInterval
30+
low: -10
31+
high: -7
2732

2833

2934
baseCommand: echo

tests/test_ext.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,3 +295,51 @@ def test_warn_large_inputs() -> None:
295295
)
296296
finally:
297297
cwltool.process.FILE_COUNT_WARNING = was
298+
299+
300+
def test_parameter_restrictions_parsing() -> None:
301+
"""Basic parsing of the ParameterRestrictions extension."""
302+
assert (
303+
main(
304+
["--enable-ext", "--validate", get_data("tests/parameter_restrictions.cwl")]
305+
)
306+
== 0
307+
)
308+
309+
310+
def test_parameter_restrictions_valid_job() -> None:
311+
"""Confirm that valid values are accepted."""
312+
assert (
313+
main(
314+
[
315+
"--enable-ext",
316+
get_data("tests/parameter_restrictions.cwl"),
317+
"--one",
318+
"2.5",
319+
"--two",
320+
"foofoobar",
321+
"--three",
322+
"-5",
323+
]
324+
)
325+
== 0
326+
)
327+
328+
329+
def test_parameter_restrictions_invalid_job() -> None:
330+
"""Confirm that an invvalid value is rejected."""
331+
assert (
332+
main(
333+
[
334+
"--enable-ext",
335+
get_data("tests/parameter_restrictions.cwl"),
336+
"--one",
337+
"2.5",
338+
"--two",
339+
"fuzzbar",
340+
"--three",
341+
"-5",
342+
]
343+
)
344+
== 1
345+
)

0 commit comments

Comments
 (0)