Skip to content

Commit 6b5b7c1

Browse files
Merge pull request #6 from InformaticsMatters/prototype-work-alan
Fix workflow validator
2 parents 7620a51 + 53ad3ca commit 6b5b7c1

File tree

5 files changed

+230
-80
lines changed

5 files changed

+230
-80
lines changed

tests/test_workflow_validator.py

Lines changed: 0 additions & 44 deletions
This file was deleted.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import os
2+
from typing import Any
3+
4+
import pytest
5+
import yaml
6+
7+
pytestmark = pytest.mark.unit
8+
9+
from tests.test_decoder import _MINIMAL_WORKFLOW
10+
from workflow.workflow_validator import ValidationLevel, WorkflowValidator
11+
12+
13+
def test_validate_minimal():
14+
# Arrange
15+
16+
# Act
17+
error = WorkflowValidator.validate(
18+
level=ValidationLevel.CREATE,
19+
workflow_definition=_MINIMAL_WORKFLOW,
20+
)
21+
22+
# Assert
23+
assert error.error_num == 0
24+
assert error.error_msg is None
25+
26+
27+
def test_validate_example_nop_file():
28+
# Arrange
29+
workflow_file: str = os.path.join(
30+
os.path.dirname(__file__), "workflow-definitions", "example-nop-fail.yaml"
31+
)
32+
with open(workflow_file, "r", encoding="utf8") as workflow_file:
33+
workflow: dict[str, Any] = yaml.load(workflow_file, Loader=yaml.FullLoader)
34+
assert workflow
35+
36+
# Act
37+
error = WorkflowValidator.validate(
38+
level=ValidationLevel.CREATE,
39+
workflow_definition=workflow,
40+
)
41+
42+
# Assert
43+
assert error.error_num == 0
44+
assert error.error_msg is None
45+
46+
47+
def test_validate_example_smiles_to_file():
48+
# Arrange
49+
workflow_file: str = os.path.join(
50+
os.path.dirname(__file__), "workflow-definitions", "example-smiles-to-file.yaml"
51+
)
52+
with open(workflow_file, "r", encoding="utf8") as workflow_file:
53+
workflow: dict[str, Any] = yaml.load(workflow_file, Loader=yaml.FullLoader)
54+
assert workflow
55+
56+
# Act
57+
error = WorkflowValidator.validate(
58+
level=ValidationLevel.CREATE,
59+
workflow_definition=workflow,
60+
)
61+
62+
# Assert
63+
assert error.error_num == 0
64+
assert error.error_msg is None
65+
66+
67+
def test_validate_example_tow_step_nop():
68+
# Arrange
69+
workflow_file: str = os.path.join(
70+
os.path.dirname(__file__), "workflow-definitions", "example-two-step-nop.yaml"
71+
)
72+
with open(workflow_file, "r", encoding="utf8") as workflow_file:
73+
workflow: dict[str, Any] = yaml.load(workflow_file, Loader=yaml.FullLoader)
74+
assert workflow
75+
76+
# Act
77+
error = WorkflowValidator.validate(
78+
level=ValidationLevel.CREATE,
79+
workflow_definition=workflow,
80+
)
81+
82+
# Assert
83+
assert error.error_num == 0
84+
assert error.error_msg is None
85+
86+
87+
def test_validate_shortcut_example_1():
88+
# Arrange
89+
workflow_file: str = os.path.join(
90+
os.path.dirname(__file__), "workflow-definitions", "shortcut-example-1.yaml"
91+
)
92+
with open(workflow_file, "r", encoding="utf8") as workflow_file:
93+
workflow: dict[str, Any] = yaml.load(workflow_file, Loader=yaml.FullLoader)
94+
assert workflow
95+
96+
# Act
97+
error = WorkflowValidator.validate(
98+
level=ValidationLevel.CREATE,
99+
workflow_definition=workflow,
100+
)
101+
102+
# Assert
103+
assert error.error_num == 0
104+
assert error.error_msg is None
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import os
2+
from typing import Any
3+
4+
import pytest
5+
import yaml
6+
7+
pytestmark = pytest.mark.unit
8+
9+
from workflow.workflow_validator import ValidationLevel, WorkflowValidator
10+
11+
12+
def test_validate_example_nop_file():
13+
# Arrange
14+
workflow_file: str = os.path.join(
15+
os.path.dirname(__file__), "workflow-definitions", "example-nop-fail.yaml"
16+
)
17+
with open(workflow_file, "r", encoding="utf8") as workflow_file:
18+
workflow: dict[str, Any] = yaml.load(workflow_file, Loader=yaml.FullLoader)
19+
assert workflow
20+
21+
# Act
22+
error = WorkflowValidator.validate(
23+
level=ValidationLevel.RUN,
24+
workflow_definition=workflow,
25+
)
26+
27+
# Assert
28+
assert error.error_num == 0
29+
assert error.error_msg is None
30+
31+
32+
def test_validate_example_smiles_to_file():
33+
# Arrange
34+
workflow_file: str = os.path.join(
35+
os.path.dirname(__file__), "workflow-definitions", "example-smiles-to-file.yaml"
36+
)
37+
with open(workflow_file, "r", encoding="utf8") as workflow_file:
38+
workflow: dict[str, Any] = yaml.load(workflow_file, Loader=yaml.FullLoader)
39+
assert workflow
40+
41+
# Act
42+
error = WorkflowValidator.validate(
43+
level=ValidationLevel.RUN,
44+
workflow_definition=workflow,
45+
)
46+
47+
# Assert
48+
assert error.error_num == 0
49+
assert error.error_msg is None
50+
51+
52+
def test_validate_example_tow_step_nop():
53+
# Arrange
54+
workflow_file: str = os.path.join(
55+
os.path.dirname(__file__), "workflow-definitions", "example-two-step-nop.yaml"
56+
)
57+
with open(workflow_file, "r", encoding="utf8") as workflow_file:
58+
workflow: dict[str, Any] = yaml.load(workflow_file, Loader=yaml.FullLoader)
59+
assert workflow
60+
61+
# Act
62+
error = WorkflowValidator.validate(
63+
level=ValidationLevel.RUN,
64+
workflow_definition=workflow,
65+
)
66+
67+
# Assert
68+
assert error.error_num == 0
69+
assert error.error_msg is None
70+
71+
72+
def test_validate_shortcut_example_1():
73+
# Arrange
74+
workflow_file: str = os.path.join(
75+
os.path.dirname(__file__), "workflow-definitions", "shortcut-example-1.yaml"
76+
)
77+
with open(workflow_file, "r", encoding="utf8") as workflow_file:
78+
workflow: dict[str, Any] = yaml.load(workflow_file, Loader=yaml.FullLoader)
79+
assert workflow
80+
81+
# Act
82+
error = WorkflowValidator.validate(
83+
level=ValidationLevel.RUN,
84+
workflow_definition=workflow,
85+
)
86+
87+
# Assert
88+
assert error.error_num == 0
89+
assert error.error_msg is None

workflow/workflow_engine.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -304,22 +304,22 @@ def _validate_step_command(
304304
# and can be launched. If it is False then the returned str contains an
305305
# error message.
306306
#
307-
# Remember that variables can exist in (ascending order of priority): -
307+
# Remember that variables can exist in the specification too.
308+
# So, the full set of step variables (in ascending order of priority)
309+
# is...
310+
#
308311
# 1. The specification
309312
# 2. The workflow
310-
# 2. The RunningWorkflow
313+
# 3. The RunningWorkflow
311314

312-
all_variables: dict[str, Any] = {}
313-
if "variables" in step_spec:
314-
all_variables = step_spec.pop("variables")
315+
all_variables = step_spec.pop("variables") if "variables" in step_spec else {}
315316
if workflow_variables:
316-
all_variables = all_variables | workflow_variables
317+
all_variables |= workflow_variables
317318
if running_workflow_variables:
318-
all_variables = all_variables | running_workflow_variables
319+
all_variables |= running_workflow_variables
319320
message, success = decode(
320321
job["command"], all_variables, "command", TextEncoding.JINJA2_3_0
321322
)
322-
323323
return all_variables if success else message
324324

325325
def _launch(

workflow/workflow_validator.py

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -72,35 +72,36 @@ def _validate_run_level(
7272
assert workflow_definition
7373
del workflow_inputs
7474

75-
# RUN level requires that the specification is a valid JSON string.
75+
# RUN level requires that each step specification is a valid JSON string.
7676
# and contains properties for 'collection', 'job', and 'version'.
77-
try:
78-
specification = json.loads(workflow_definition["specification"])
79-
except json.decoder.JSONDecodeError as e:
80-
return ValidationResult(
81-
error_num=1,
82-
error_msg=[
83-
f"Error decoding specification, which is not valid JSON: {e}"
84-
],
85-
)
86-
except TypeError as e:
87-
return ValidationResult(
88-
error_num=2,
89-
error_msg=[
90-
f"Error decoding specification, which is not valid JSON: {e}"
91-
],
92-
)
93-
expected_keys: set[str] = {"collection", "job", "version"}
94-
missing_keys: list[str] = []
95-
missing_keys.extend(
96-
expected_key
97-
for expected_key in expected_keys
98-
if expected_key not in specification
99-
)
100-
if missing_keys:
101-
return ValidationResult(
102-
error_num=2,
103-
error_msg=[f"Specification is missing: {', '.join(missing_keys)}"],
77+
for step in workflow_definition["steps"]:
78+
try:
79+
specification = json.loads(step["specification"])
80+
except json.decoder.JSONDecodeError as e:
81+
return ValidationResult(
82+
error_num=2,
83+
error_msg=[
84+
f"Error decoding specification, which is not valid JSON: {e}"
85+
],
86+
)
87+
except TypeError as e:
88+
return ValidationResult(
89+
error_num=3,
90+
error_msg=[
91+
f"Error decoding specification, which is not valid JSON: {e}"
92+
],
93+
)
94+
expected_keys: set[str] = {"collection", "job", "version"}
95+
missing_keys: list[str] = []
96+
missing_keys.extend(
97+
expected_key
98+
for expected_key in expected_keys
99+
if expected_key not in specification
104100
)
101+
if missing_keys:
102+
return ValidationResult(
103+
error_num=2,
104+
error_msg=[f"Specification is missing: {', '.join(missing_keys)}"],
105+
)
105106

106107
return _VALIDATION_SUCCESS

0 commit comments

Comments
 (0)