Skip to content

Commit 7fb61b9

Browse files
authored
Merge branch 'main' into nextflow_improvements
2 parents 805d221 + b9b4554 commit 7fb61b9

81 files changed

Lines changed: 3135 additions & 1453 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build.yml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,26 @@ jobs:
1818

1919
- name: Install dependencies
2020
run: |
21+
sudo apt-get update
2122
sudo apt-get install -y python3-sphinx sphinx-rtd-theme-common
2223
pip install sphinx_rtd_theme recommonmark pytest pytest-cov
24+
pip install --upgrade setuptools
25+
26+
- name: Install utilities for testing
27+
run: |
28+
sudo apt-get update
29+
sudo apt-get install stress-ng
30+
sudo apt-get install graphviz libgraphviz-dev
31+
pip install docker
32+
pip install pygraphviz
33+
pip install pydot
2334
2435
- name: Check package install
2536
run: |
26-
pip install -e .
37+
pip install .
2738
2839
- name: Run tests
29-
run: pytest -m unit --cov=wfcommons tests/
40+
run: python3 -m pytest -s -v -m unit --cov=wfcommons tests/
3041

3142
- name: Upload coverage
3243
if: github.ref == 'refs/heads/main'

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,8 @@ bin/gpu_benchmark
5555
/wfcommons/wfc-1.3_to_dask/out
5656
/wfcommons/wfc-1.3_to_dask/htmlcov
5757

58+
# Coverage stuff
59+
.coverage
60+
coverage.xml
61+
5862
test/

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ This Python package provides a collection of tools for:
2121
## Installation
2222

2323
WfCommons is available on [PyPI](https://pypi.org/project/wfcommons).
24-
WfCommons requires Python3.10+ and has been tested on Linux and macOS.
24+
WfCommons requires Python3.11+ and has been tested on Linux and MacOS.
2525

2626
### Installation using pip
2727

bin/wfbench

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ def begin_flowcept(args):
341341
bundle_exec_id=args.workflow_id,
342342
start_persistence=False, save_workflow=False)
343343
f.start()
344-
t = FlowceptTask(workflow_id=args.workflow_id, used={**args.__dict__})
344+
t = FlowceptTask(task_id=f"{args.workflow_id}_{args.name}", workflow_id=args.workflow_id, used={**args.__dict__})
345345
return f, t
346346

347347

@@ -457,7 +457,11 @@ def main():
457457
if args.time:
458458
time.sleep(int(args.time))
459459
for proc in procs:
460-
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
460+
if isinstance(proc, multiprocessing.Process):
461+
if proc.is_alive():
462+
proc.terminate()
463+
elif isinstance(proc, subprocess.Popen):
464+
proc.terminate()
461465
else:
462466
for proc in procs:
463467
if isinstance(proc, subprocess.Popen):

docs/source/quickstart_installation.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Installation
22
============
33

44
WfCommons is available on `PyPI <https://pypi.org/project/wfcommons>`_.
5-
WfCommons requires Python3.10+ and has been tested on Linux and macOS.
5+
WfCommons requires Python3.11+ and has been tested on Linux and macOS.
66

77
Installation using pip
88
----------------------

pyproject.toml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@ name = "wfcommons"
77
authors = [{name = "WfCommons team", email = "support@wfcommons.org"}]
88
description = "A Framework for Enabling Scientific Workflow Research and Education"
99
readme = "README.md"
10-
requires-python = ">=3.10"
10+
requires-python = ">=3.11"
1111
classifiers = [
1212
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
1313
"Operating System :: OS Independent",
14-
"Programming Language :: Python :: 3",
15-
"Programming Language :: Python :: 3.10",
1614
"Programming Language :: Python :: 3.11",
1715
"Programming Language :: Python :: 3.12",
1816
"Programming Language :: Python :: 3.13",
@@ -33,7 +31,7 @@ dependencies = [
3331
"pandas",
3432
"python-dateutil",
3533
"requests",
36-
"scipy",
34+
"scipy>=1.16.1",
3735
"pyyaml",
3836
"pandas",
3937
"shortuuid",
@@ -56,14 +54,19 @@ flowcept = ["flowcept"]
5654
[tool.setuptools.dynamic]
5755
version = {attr = "wfcommons.version.__version__"}
5856

57+
[tool.coverage.run]
58+
omit = [
59+
"wfcommons/wfinstances/logs/pegasusrec.py",
60+
"wfcommons/wfbench/translator/templates"
61+
]
62+
5963
[tool.pytest.ini_options]
6064
addopts="""
6165
--cov-context test \
6266
--cov-config pyproject.toml \
6367
--cov-report xml:coverage.xml \
6468
--cov-report term-missing \
6569
--cov ./wfcommons \
66-
--ignore wfcommons/wfbench/translator/templates \
6770
--no-cov-on-fail \
6871
-ra \
6972
-W ignore"""

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
[metadata]
2-
description-file = README.md
2+
description_file = README.md

setup.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,13 @@
1414
from setuptools import setup, find_packages
1515
from setuptools.command.build_ext import build_ext
1616

17-
1817
class Build(build_ext):
1918
"""Customized setuptools build command - builds protos on build."""
2019

2120
def run(self):
2221
protoc_command = ["make"]
2322
if subprocess.call(protoc_command) != 0:
24-
print("Error: 'make' is not istnalled. Please install 'make' and try again.")
23+
sys.stderr.write("Error: 'make' is not installed. Please install 'make' and try again.\n")
2524
sys.exit(-1)
2625
super().run()
2726

@@ -33,11 +32,14 @@ def run(self):
3332
'build_ext': Build,
3433
},
3534
data_files=[
36-
('bin', ['bin/cpu-benchmark', 'bin/wfbench'])
35+
('bin', ['bin/cpu-benchmark'])
36+
],
37+
scripts=[
38+
'bin/wfbench'
3739
],
3840
entry_points={
3941
'console_scripts': [
40-
'wfchef=wfcommons.wfchef.chef:main'
42+
'wfchef=wfcommons.wfchef.chef:main',
4143
],
4244
'workflow_recipes': [
4345
'epigenomics_recipe = wfcommons.wfchef.recipes:EpigenomicsRecipe',

tests/recipes/test_recipes.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
#
4+
# Copyright (c) 2025 The WfCommons Team.
5+
#
6+
# This program is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
11+
import pathlib
12+
import pytest
13+
import wfcommons.utils
14+
15+
from typing import Dict, List, Tuple
16+
from wfcommons.wfchef.recipes import GenomeRecipe
17+
from wfcommons.wfchef.recipes import SeismologyRecipe
18+
from wfcommons.wfchef.recipes import MontageRecipe
19+
from wfcommons.wfchef.recipes import RnaseqRecipe
20+
from wfcommons.wfchef.recipes import BwaRecipe
21+
from wfcommons.wfchef.recipes import SoykbRecipe
22+
from wfcommons.wfchef.recipes import CyclesRecipe
23+
from wfcommons.wfchef.recipes import BlastRecipe
24+
from wfcommons.wfchef.recipes import EpigenomicsRecipe
25+
from wfcommons.wfchef.recipes import SrasearchRecipe
26+
27+
from wfcommons import WorkflowGenerator
28+
29+
class TestRecipes:
30+
31+
recipe_class_list = [
32+
GenomeRecipe,
33+
SeismologyRecipe,
34+
MontageRecipe,
35+
RnaseqRecipe,
36+
BwaRecipe,
37+
SoykbRecipe,
38+
CyclesRecipe,
39+
BlastRecipe,
40+
EpigenomicsRecipe,
41+
SrasearchRecipe,
42+
]
43+
44+
@pytest.mark.unit
45+
@pytest.mark.parametrize(
46+
"recipe_class",
47+
recipe_class_list
48+
)
49+
def test_recipes(self, recipe_class) -> None:
50+
51+
recipe = recipe_class.from_num_tasks(num_tasks=200, runtime_factor=1.1, input_file_size_factor=1.5,
52+
output_file_size_factor=0.8)
53+
workflows = WorkflowGenerator(recipe).build_workflows(1)
54+
assert len(workflows) == 1
55+
56+
57+
58+
@pytest.mark.unit
59+
@pytest.mark.parametrize(
60+
"recipe_class",
61+
recipe_class_list
62+
)
63+
def test_recipes_errors(self, recipe_class) -> None:
64+
# Not enough tasks
65+
recipe = recipe_class.from_num_tasks(num_tasks=2, runtime_factor=1.1, input_file_size_factor=1.5,
66+
output_file_size_factor=0.8)
67+
with pytest.raises(ValueError):
68+
WorkflowGenerator(recipe).build_workflow()
69+
70+
# Bogus parameters
71+
with pytest.raises(ValueError):
72+
recipe_class.from_num_tasks(num_tasks=2, runtime_factor=-1.1, input_file_size_factor=1.5,
73+
output_file_size_factor=0.8)
74+
with pytest.raises(ValueError):
75+
recipe_class.from_num_tasks(num_tasks=2, runtime_factor=1.1, input_file_size_factor=-1.5,
76+
output_file_size_factor=0.8)
77+
with pytest.raises(ValueError):
78+
recipe_class.from_num_tasks(num_tasks=2, runtime_factor=1.1, input_file_size_factor=-1.5,
79+
output_file_size_factor=-0.8)

tests/test_helpers.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import pathlib
2+
import shutil
3+
import tarfile
4+
import os
5+
import io
6+
import sys
7+
import docker
8+
import networkx
9+
from docker.errors import ImageNotFound
10+
11+
from wfcommons.common import Workflow
12+
13+
14+
def _create_fresh_local_dir(path: str) -> pathlib.Path:
15+
dirpath = pathlib.Path(path)
16+
if dirpath.exists():
17+
shutil.rmtree(dirpath)
18+
dirpath.mkdir(parents=True, exist_ok=True)
19+
return dirpath
20+
21+
def _remove_local_dir_if_it_exists(path: str) -> None:
22+
dirpath = pathlib.Path(path)
23+
if dirpath.exists():
24+
shutil.rmtree(dirpath)
25+
26+
27+
def _make_tarfile_of_wfcommons():
28+
source_dir = os.getcwd() # This assumes the testing is run from the root
29+
tar_stream = io.BytesIO()
30+
with tarfile.open(fileobj=tar_stream, mode='w') as tar:
31+
tar.add(source_dir, arcname=os.path.basename(source_dir))
32+
tar_stream.seek(0)
33+
return tar_stream
34+
35+
36+
def _install_WfCommons_on_container(container):
37+
# sys.stderr.write("Installing WfCommons on the container...\n")
38+
# Copy the WfCommons code to it (removing stuff that should be removed)
39+
target_path = '/tmp/' # inside container
40+
tar_data = _make_tarfile_of_wfcommons()
41+
container.put_archive(target_path, tar_data)
42+
# Cleanup files from the host
43+
exit_code, output = container.exec_run("sudo /bin/rm -rf /tmp/WfCommons/build/", stdout=True, stderr=True)
44+
exit_code, output = container.exec_run("sudo /bin/rm -rf /tmp/WfCommons/*.egg-info/", stdout=True, stderr=True)
45+
exit_code, output = container.exec_run("sudo /bin/rm -rf /tmp/WfCommons/bin/cpu-benchmark.o", stdout=True,
46+
stderr=True)
47+
exit_code, output = container.exec_run("sudo /bin/rm -rf /tmp/WfCommons/bin/cpu-benchmark", stdout=True,
48+
stderr=True)
49+
50+
# Install WfCommons on the container (to install wfbench and cpu-benchmark really)
51+
exit_code, output = container.exec_run("sudo python3 -m pip install . --break-system-packages",
52+
workdir="/tmp/WfCommons", stdout=True, stderr=True)
53+
if exit_code != 0:
54+
raise RuntimeError("Failed to install WfCommons on the container")
55+
56+
def _start_docker_container(backend, mounted_dir, working_dir, bin_dir, command=None):
57+
if command is None:
58+
command = ["sleep", "infinity"]
59+
# Pulling the Docker image
60+
client = docker.from_env()
61+
image_name = f"wfcommons/wfcommons-testing-{backend}"
62+
63+
try:
64+
image = client.images.get(image_name)
65+
sys.stderr.write(f"[{backend}] Image '{image_name}' is available locally\n")
66+
except ImageNotFound:
67+
sys.stderr.write(f"[{backend}] Pulling image '{image_name}'...\n")
68+
client.images.pull(image_name)
69+
70+
# Launch the docker container to actually run the translated workflow
71+
sys.stderr.write(f"[{backend}] Starting Docker container...\n")
72+
container = client.containers.run(
73+
image=image_name,
74+
command=command,
75+
volumes={mounted_dir: {'bind': mounted_dir, 'mode': 'rw'}},
76+
working_dir=working_dir,
77+
tty=True,
78+
detach=True
79+
)
80+
81+
# Installing WfCommons on container
82+
_install_WfCommons_on_container(container)
83+
84+
# Copy over the wfbench and cpu-benchmark executables to where they should go on the container
85+
if bin_dir:
86+
sys.stderr.write(f"[{backend}] Copying wfbench and cpu-benchmark...\n")
87+
exit_code, output = container.exec_run(["sh", "-c", "sudo cp -f `which wfbench` " + bin_dir],
88+
stdout=True, stderr=True)
89+
if exit_code != 0:
90+
raise RuntimeError("Failed to copy wfbench script to the bin directory")
91+
exit_code, output = container.exec_run(["sh", "-c", "sudo cp -f `which cpu-benchmark` " + bin_dir],
92+
stdout=True, stderr=True)
93+
if exit_code != 0:
94+
raise RuntimeError("Failed to copy cpu-benchmark executable to the bin directory")
95+
else:
96+
sys.stderr.write(f"[{backend}] Not Copying wfbench and cpu-benchmark...\n")
97+
98+
container.backend = backend
99+
return container
100+
101+
def _shutdown_docker_container_and_remove_image(container):
102+
image = container.image
103+
sys.stderr.write(f"[{container.backend}] Terminating container if need be...\n")
104+
try:
105+
container.stop()
106+
container.remove()
107+
except Exception as e:
108+
pass
109+
110+
# Remove the images as we go, if running on GitHub
111+
if os.getenv('CI') or os.getenv('GITHUB_ACTIONS'):
112+
sys.stderr.write(f"[{container.backend}] Removing Docker image...\n")
113+
try:
114+
image.remove(force=True)
115+
except Exception as e:
116+
sys.stderr.write(f"[{container.backend}] Warning: Error while removing image: {e}\n")
117+
118+
def _get_total_size_of_directory(directory_path: str):
119+
total_size = 0
120+
for dirpath, dirnames, filenames in os.walk(directory_path):
121+
for filename in filenames:
122+
filepath = os.path.join(dirpath, filename)
123+
total_size += os.path.getsize(filepath)
124+
return total_size
125+
126+
def _compare_workflows(workflow1: Workflow, workflow_2: Workflow):
127+
128+
# Test the number of tasks
129+
assert (len(workflow1.tasks) == len(workflow_2.tasks))
130+
# Test the task graph topology
131+
assert (networkx.is_isomorphic(workflow1, workflow_2))
132+
# Test the total file size sum
133+
workflow1_input_bytes, workflow2_input_bytes = 0, 0
134+
workflow1_output_bytes, workflow2_output_bytes = 0, 0
135+
for workflow1_task, workflow2_task in zip(workflow1.tasks.values(), workflow_2.tasks.values()):
136+
# sys.stderr.write(f"WORKFLOW1: {workflow1_task.task_id} WORKFLOW2 TASK: {workflow2_task.task_id}\n")
137+
for input_file in workflow1_task.input_files:
138+
# sys.stderr.write(f"WORKFLOW1 INPUT FILE: {input_file.file_id} {input_file.size}\n")
139+
workflow1_input_bytes += input_file.size
140+
for input_file in workflow2_task.input_files:
141+
# sys.stderr.write(f"WORKFLOW2 INPUT FILE: {input_file.file_id} {input_file.size}\n")
142+
workflow2_input_bytes += input_file.size
143+
for output_file in workflow1_task.output_files:
144+
# sys.stderr.write(f"WORKFLOW1 OUTPUT FILE: {output_file.file_id} {output_file.size}\n")
145+
workflow1_output_bytes += output_file.size
146+
for output_file in workflow2_task.output_files:
147+
# sys.stderr.write(f"WORKFLOW2 OUTPUT FILE: {output_file.file_id} {output_file.size}\n")
148+
workflow2_output_bytes += output_file.size
149+
assert (workflow1_input_bytes == workflow2_input_bytes)
150+
assert (workflow1_output_bytes == workflow2_output_bytes)

0 commit comments

Comments
 (0)