diff --git a/cwl_utils/parser/cwl_v1_2.py b/cwl_utils/parser/cwl_v1_2.py index b24ac23c..db6bc9b2 100644 --- a/cwl_utils/parser/cwl_v1_2.py +++ b/cwl_utils/parser/cwl_v1_2.py @@ -18131,6 +18131,14 @@ def fromDoc( "is not valid because:", ) ) + if coresMin is not None and coresMax is not None and coresMin > coresMax: + _errors__.append( + ValidationException( + "the `coresMin` field is greater than the `coresMax` field", + None + ) + ) + ramMin = None if "ramMin" in _doc: try: @@ -18225,6 +18233,14 @@ def fromDoc( "is not valid because:", ) ) + if ramMin is not None and ramMax is not None and ramMin > ramMax: + _errors__.append( + ValidationException( + "the `ramMin` field is greater than the `ramMax` field", + None + ) + ) + tmpdirMin = None if "tmpdirMin" in _doc: try: @@ -18319,6 +18335,14 @@ def fromDoc( "is not valid because:", ) ) + if tmpdirMin is not None and tmpdirMax is not None and tmpdirMin > tmpdirMax: + _errors__.append( + ValidationException( + "the `tmpdirMin` field is greater than the `tmpdirMax` field", + None + ) + ) + outdirMin = None if "outdirMin" in _doc: try: @@ -18413,6 +18437,14 @@ def fromDoc( "is not valid because:", ) ) + if outdirMin is not None and outdirMax is not None and outdirMin > outdirMax: + _errors__.append( + ValidationException( + "the `outdirMin` field is greater than the `outdirMax` field", + None + ) + ) + extension_fields: dict[str, Any] = {} for k in _doc.keys(): if k not in cls.attrs: @@ -18478,6 +18510,11 @@ def save( r["coresMax"] = save( self.coresMax, top=False, base_url=base_url, relative_uris=relative_uris ) + if r.get("coresMax") and r.get("coresMin") and r["coresMax"] < r["coresMin"]: + raise ValidationException( + "the `coresMax` field is less than the `coresMin` field" + ) + if self.ramMin is not None: r["ramMin"] = save( self.ramMin, top=False, base_url=base_url, relative_uris=relative_uris @@ -18486,6 +18523,11 @@ def save( r["ramMax"] = save( self.ramMax, top=False, base_url=base_url, relative_uris=relative_uris ) + if r.get("ramMax") and r.get("ramMin") and r["ramMax"] < r["ramMin"]: + raise ValidationException( + "the `ramMax` field is less than the `ramMin` field" + ) + if self.tmpdirMin is not None: r["tmpdirMin"] = save( self.tmpdirMin, @@ -18500,6 +18542,11 @@ def save( base_url=base_url, relative_uris=relative_uris, ) + if r.get("tmpdirMax") and r.get("tmpdirMin") and r["tmpdirMax"] < r["tmpdirMin"]: + raise ValidationException( + "the `tmpdirMax` field is less than the `tmpdirMin` field" + ) + if self.outdirMin is not None: r["outdirMin"] = save( self.outdirMin, @@ -18514,6 +18561,10 @@ def save( base_url=base_url, relative_uris=relative_uris, ) + if r.get("outdirMax") and r.get("outdirMin") and r["outdirMax"] < r["outdirMin"]: + raise ValidationException( + "the `outdirMax` field is less than the `outdirMin` field" + ) # top refers to the directory level if top: diff --git a/tests/test_resreq_minmax.py b/tests/test_resreq_minmax.py new file mode 100644 index 00000000..81f214a2 --- /dev/null +++ b/tests/test_resreq_minmax.py @@ -0,0 +1,108 @@ +from typing import Optional, List, Any + +from cwl_utils.parser.cwl_v1_2 import ( + ResourceRequirement, + WorkflowStep, + Workflow, + CommandLineTool, + save, +) +import pytest +from schema_salad.exceptions import ValidationException + + +# Helper functions +def create_commandlinetool( + requirements: Optional[List[Any]] = None, + inputs: Optional[List[Any]] = None, + outputs: Optional[List[Any]] = None, +) -> CommandLineTool: + return CommandLineTool( + requirements=requirements or [], + inputs=inputs or [], + outputs=outputs or [], + ) + + +def create_workflow( + requirements: Optional[List[Any]] = None, + steps: Optional[List[Any]] = None, + inputs: Optional[List[Any]] = None, + outputs: Optional[List[Any]] = None, +) -> Workflow: + return Workflow( + requirements=requirements or [], + steps=steps or [], + inputs=inputs or [], + outputs=outputs or [], + ) + + +def create_step( + requirements: Optional[List[Any]] = None, + run: Any = None, + in_: Optional[List[Any]] = None, + out: Optional[List[Any]] = None, +) -> WorkflowStep: + return WorkflowStep( + requirements=requirements or [], + run=run, + in_=in_ or [], + out=out or [], + ) + + +@pytest.mark.parametrize( + "bad_min_max_reqs", + [ + # cores + ResourceRequirement(coresMin=4, coresMax=2), + # ram + ResourceRequirement(ramMin=2048, ramMax=1024), + # tmpdir + ResourceRequirement(tmpdirMin=1024, tmpdirMax=512), + # outdir + ResourceRequirement(outdirMin=512, outdirMax=256), + ], +) +def test_bad_min_max_resource_reqs(bad_min_max_reqs: ResourceRequirement) -> None: + # Test invalid min/max resource requirements in CWL objects. + + # CommandlineTool with bad minmax reqs + clt = create_commandlinetool(requirements=[bad_min_max_reqs]) + with pytest.raises(ValidationException): + save(clt) + + # WorkflowStep.run with bad minmax reqs + step_bad_run = create_step(run=clt) + workflow = create_workflow(steps=[step_bad_run]) + with pytest.raises(ValidationException): + save(workflow) + + # WorkflowStep with bad minmax reqs + clt = create_commandlinetool() + step = create_step(run=clt, requirements=[bad_min_max_reqs]) + workflow = create_workflow(steps=[step]) + with pytest.raises(ValidationException): + save(workflow) + + # Workflow with bad minmax reqs + workflow = create_workflow(requirements=[bad_min_max_reqs]) + with pytest.raises(ValidationException): + save(workflow) + + # NestedWorkflow with bad minmax reqs + nest_workflow = create_workflow(requirements=[bad_min_max_reqs]) + step = create_step(run=nest_workflow) + workflow = create_workflow(steps=[step]) + with pytest.raises(ValidationException): + save(workflow) + + # DeepNestedWorkflow with bad minmax reqs + deep_workflow = create_workflow(requirements=[bad_min_max_reqs]) + deep_step = create_step(run=deep_workflow) + nest_workflow = create_workflow(steps=[deep_step]) + step = create_step(run=nest_workflow) + workflow = create_workflow(steps=[step]) + with pytest.raises(ValidationException): + save(workflow)