Skip to content

Commit 7e9be1d

Browse files
committed
Completed move to WfFormat 1.6
- Bug fixes - Test fixes
1 parent 79b4ef4 commit 7e9be1d

3 files changed

Lines changed: 216 additions & 32 deletions

File tree

tests/unit/common/test_workflow.py

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
import pytest
1313
import requests
1414
import json
15+
import math
16+
from numbers import Real
17+
from typing import Any
1518

1619
from datetime import datetime
1720
from wfcommons.common import Task, Workflow
@@ -26,6 +29,97 @@
2629
]
2730

2831

32+
def assert_json_close(
33+
actual: Any,
34+
expected: Any,
35+
*,
36+
rel_tol: float = 1e-9,
37+
abs_tol: float = 1e-12,
38+
path: str = "$",
39+
) -> None:
40+
"""Assert that two JSON-like objects are equal, with close numerics."""
41+
42+
actual_is_number = (
43+
isinstance(actual, Real)
44+
and not isinstance(actual, bool)
45+
)
46+
expected_is_number = (
47+
isinstance(expected, Real)
48+
and not isinstance(expected, bool)
49+
)
50+
51+
if actual_is_number or expected_is_number:
52+
assert actual_is_number and expected_is_number, (
53+
f"Numeric type mismatch at {path}: "
54+
f"actual={actual!r}, expected={expected!r}"
55+
)
56+
57+
assert math.isclose(
58+
actual,
59+
expected,
60+
rel_tol=rel_tol,
61+
abs_tol=abs_tol,
62+
), (
63+
f"Numbers differ at {path}: "
64+
f"actual={actual!r}, expected={expected!r}, "
65+
f"rel_tol={rel_tol}, abs_tol={abs_tol}"
66+
)
67+
return
68+
69+
if isinstance(actual, dict) or isinstance(expected, dict):
70+
assert isinstance(actual, dict) and isinstance(expected, dict), (
71+
f"Type mismatch at {path}: "
72+
f"actual={type(actual).__name__}, "
73+
f"expected={type(expected).__name__}"
74+
)
75+
76+
assert actual.keys() == expected.keys(), (
77+
f"Dictionary keys differ at {path}: "
78+
f"actual-only={actual.keys() - expected.keys()}, "
79+
f"expected-only={expected.keys() - actual.keys()}"
80+
)
81+
82+
for key in actual:
83+
assert_json_close(
84+
actual[key],
85+
expected[key],
86+
rel_tol=rel_tol,
87+
abs_tol=abs_tol,
88+
path=f"{path}.{key}",
89+
)
90+
return
91+
92+
if isinstance(actual, list) or isinstance(expected, list):
93+
assert isinstance(actual, list) and isinstance(expected, list), (
94+
f"Type mismatch at {path}: "
95+
f"actual={type(actual).__name__}, "
96+
f"expected={type(expected).__name__}"
97+
)
98+
99+
assert len(actual) == len(expected), (
100+
f"List lengths differ at {path}: "
101+
f"actual={len(actual)}, expected={len(expected)}"
102+
)
103+
104+
for index, (actual_item, expected_item) in enumerate(
105+
zip(actual, expected)
106+
):
107+
assert_json_close(
108+
actual_item,
109+
expected_item,
110+
rel_tol=rel_tol,
111+
abs_tol=abs_tol,
112+
path=f"{path}[{index}]",
113+
)
114+
return
115+
116+
assert actual == expected, (
117+
f"Values differ at {path}: "
118+
f"actual={actual!r}, expected={expected!r}"
119+
)
120+
121+
122+
29123
class TestWorkflow:
30124

31125
@pytest.fixture
@@ -53,7 +147,11 @@ def test_workflow_creation(self, workflow: Workflow) -> None:
53147
"workflow": {
54148
"specification": {
55149
"tasks": [],
56-
"files": []
150+
"files": [],
151+
"metrics": {
152+
"numberOfTasks": 0,
153+
"numberOfFiles": 0
154+
}
57155
},
58156
"execution": {
59157
"makespanInSeconds": 100.0,
@@ -139,7 +237,9 @@ def test_workflow_json_generation(self):
139237
written_json["workflow"]["specification"]["files"] = sorted(written_json["workflow"]["specification"]["files"], key=lambda x: x['id'])
140238

141239
# Compare the two jsons!
142-
assert(original_json == written_json)
240+
# assert(original_json == written_json)
241+
# assert(original_json == pytest.approx(written_json))
242+
assert_json_close(original_json,written_json)
143243

144244
@pytest.mark.unit
145245
def test_workflow_dot_file(self):

wfcommons/common/workflow.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from ..common.task import Task, TaskType
2020
from ..version import __version__, __schema_version__
2121

22-
from ..wfchef.utils import create_graph
22+
from ..wfchef.utils import create_graph, create_graph_from_json_object
2323
import tempfile
2424

2525

@@ -210,6 +210,75 @@ def generate_json(self) -> None:
210210

211211
self.workflow_json = workflow_json
212212

213+
# Augment the workflow_json with the metrics
214+
specification_metrics, execution_metrics = self.compute_metrics(workflow_json)
215+
if specification_metrics != {}:
216+
self.workflow_json["workflow"]["specification"]["metrics"] = specification_metrics
217+
if execution_metrics != {}:
218+
self.workflow_json["workflow"]["execution"]["metrics"] = execution_metrics
219+
220+
221+
def compute_metrics(self, workflow_json: json) -> json:
222+
"""
223+
Compute workflow specification and execution metrics.
224+
:param workflow_json: Workflow instance as a JSON object.
225+
:type workflow_json: json
226+
227+
:return: specification and execution metrics.
228+
:rtype: json
229+
"""
230+
231+
# Specification metrics
232+
specification_metrics : json = {}
233+
specification_metrics["numberOfTasks"] = len(workflow_json["workflow"]["specification"]["tasks"])
234+
if "files" in workflow_json["workflow"]["specification"]:
235+
number_of_files = len(workflow_json["workflow"]["specification"]["files"])
236+
specification_metrics["numberOfFiles"] = number_of_files
237+
if number_of_files > 0:
238+
specification_metrics["sumOfFileSizesInBytes"] = sum(
239+
file.get("sizeInBytes", 0)
240+
for file in workflow_json["workflow"]["specification"]["files"]
241+
)
242+
graph = create_graph_from_json_object(workflow_json)
243+
levels = [
244+
tuple(generation)
245+
for generation in nx.topological_generations(graph)
246+
][1:-1] # Remove the fictitious SRC and DST levels
247+
248+
if len(levels) > 0:
249+
widths = [len(level) for level in levels] # Remove the fictitious SRC and DST levels
250+
251+
specification_metrics["numberOfLevels"] = len(widths)
252+
specification_metrics["minimumWidth"] = min(widths)
253+
specification_metrics["maximumWidth"] = max(widths)
254+
255+
# Execution metrics
256+
execution_metrics : json = {}
257+
if "execution" in workflow_json["workflow"]:
258+
total_runtime_in_seconds = sum(
259+
task.get("runtimeInSeconds", 0)
260+
for task in workflow_json["workflow"]["execution"]["tasks"]
261+
)
262+
if total_runtime_in_seconds > 0:
263+
execution_metrics["sumTaskRuntimesInSeconds"] = total_runtime_in_seconds
264+
265+
total_read_bytes = sum(
266+
task.get("readBytes", 0)
267+
for task in workflow_json["workflow"]["execution"]["tasks"]
268+
)
269+
if total_runtime_in_seconds > 0:
270+
execution_metrics["totalNumBytesRead"] = total_read_bytes
271+
272+
total_written_bytes = sum(
273+
task.get("writtenBytes", 0)
274+
for task in workflow_json["workflow"]["execution"]["tasks"]
275+
)
276+
if total_written_bytes > 0:
277+
execution_metrics["totalNumBytesWritten"] = total_written_bytes
278+
279+
return specification_metrics, execution_metrics
280+
281+
213282
def write_dot(self, dot_file_path: Optional[pathlib.Path] = None) -> None:
214283
"""
215284
Write a dot file of the workflow instance.
@@ -264,3 +333,8 @@ def roots(self) -> List[str]:
264333

265334
def leaves(self) -> List[str]:
266335
return [n for n,d in self.out_degree() if d==0]
336+
337+
338+
339+
340+

wfcommons/wfchef/utils.py

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -47,42 +47,54 @@ def create_graph(path: pathlib.Path) -> nx.DiGraph:
4747
path = pathlib.Path(path)
4848
with path.open() as fp:
4949
content = json.load(fp)
50+
return create_graph_from_json_object(content)
5051

51-
graph = nx.DiGraph()
5252

53-
# Add src/dst nodes
54-
graph.add_node("SRC", label="SRC", type="SRC", id="SRC")
55-
graph.add_node("DST", label="DST", type="DST", id="DST")
53+
def create_graph_from_json_object(workflow_instance: dict) -> nx.DiGraph:
54+
"""
55+
Creates a networkX DiGraph from a JSON file in the WfFormat.
5656
57-
id_count = 0
57+
:param workflow_instance: A workflow instance as a JSON object.
58+
:type workflow_instance: json
5859
59-
for task in content["workflow"]["specification"]["tasks"]:
60+
:return: graph.
61+
:rtype: networkX DiGraph.
62+
"""
63+
graph = nx.DiGraph()
6064

61-
# specific for epigenomics -- have to think about how to do it in general
62-
if "genome-dax" in content["name"]:
63-
_type, *_ = task["name"].split("_")
64-
graph.add_node(task["name"], label=_type, type=_type, id=str(id_count))
65-
id_count += 1
66-
else:
67-
try:
68-
_type, _id = task["name"].split("_ID")
69-
except ValueError:
70-
_type, _id = task["name"].split("_0")
71-
graph.add_node(task["name"], label=_type, type=_type, id=_id)
65+
# Add src/dst nodes
66+
graph.add_node("SRC", label="SRC", type="SRC", id="SRC")
67+
graph.add_node("DST", label="DST", type="DST", id="DST")
7268

73-
for parent in task["parents"]:
74-
graph.add_edge(parent, task["name"])
69+
id_count = 0
7570

76-
for node in graph.nodes:
71+
for task in workflow_instance["workflow"]["specification"]["tasks"]:
7772

78-
if node in ["SRC", "DST"]:
79-
continue
80-
if graph.in_degree(node) <= 0:
81-
graph.add_edge("SRC", node)
82-
if graph.out_degree(node) <= 0:
83-
graph.add_edge(node, "DST")
73+
# specific for epigenomics -- have to think about how to do it in general
74+
if "genome-dax" in workflow_instance["name"]:
75+
_type, *_ = task["name"].split("_")
76+
graph.add_node(task["name"], label=_type, type=_type, id=str(id_count))
77+
id_count += 1
78+
else:
79+
try:
80+
_type, _id = task["id"].split("_ID")
81+
except ValueError:
82+
_type, _id = task["id"].split("_0")
83+
graph.add_node(task["name"], label=_type, type=_type, id=_id)
8484

85-
return graph
85+
for parent in task["parents"]:
86+
graph.add_edge(parent, task["name"])
87+
88+
for node in graph.nodes:
89+
90+
if node in ["SRC", "DST"]:
91+
continue
92+
if graph.in_degree(node) <= 0:
93+
graph.add_edge("SRC", node)
94+
if graph.out_degree(node) <= 0:
95+
graph.add_edge(node, "DST")
96+
97+
return graph
8698

8799

88100
def annotate(g: nx.DiGraph) -> None:
@@ -176,8 +188,6 @@ def draw(g: nx.DiGraph,
176188
:param subgraph: nodes that were added by replication and will be colored green.
177189
:type subgraph: Set[str].
178190
179-
180-
181191
:return: the figure and the axis used.
182192
:rtype: Tuple[plt.Figure, plt.Axes].
183193
"""

0 commit comments

Comments
 (0)