175 lines
5.5 KiB
Python
175 lines
5.5 KiB
Python
"""动态任务生成(dynamic=True)测试。
|
|
|
|
覆盖:
|
|
* 基本动态生成:fn 返回单个 spec / list[spec]。
|
|
* 生成任务 result.value 被替换为生成任务名列表。
|
|
* 新 spec 自动依赖生成方。
|
|
* 多级动态生成(生成的任务再生成)。
|
|
* 非 dependency 策略下 dynamic=True 抛 ValueError。
|
|
* 非动态任务不受影响。
|
|
* 空列表 / None 返回值不生成任务。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
import pyflowx as px
|
|
from pyflowx import Graph, TaskSpec
|
|
|
|
|
|
def test_dynamic_single_spec() -> None:
|
|
"""fn 返回单个 TaskSpec,动态注册并执行。"""
|
|
|
|
def generator() -> TaskSpec:
|
|
return TaskSpec("spawned", fn=lambda: 42)
|
|
|
|
graph = Graph.from_specs([TaskSpec("gen", fn=generator, dynamic=True)])
|
|
report = px.run(graph, strategy="dependency")
|
|
assert report.success
|
|
assert "spawned" in report.results
|
|
assert report["spawned"] == 42
|
|
# 生成任务的 result.value 被替换为生成任务名列表
|
|
assert report["gen"] == ["spawned"]
|
|
|
|
|
|
def test_dynamic_list_of_specs() -> None:
|
|
"""fn 返回 list[TaskSpec],全部注册并执行。"""
|
|
|
|
def generator() -> list[TaskSpec]:
|
|
return [TaskSpec(f"task_{i}", fn=lambda i=i: i * 10) for i in range(3)]
|
|
|
|
graph = Graph.from_specs([TaskSpec("gen", fn=generator, dynamic=True)])
|
|
report = px.run(graph, strategy="dependency")
|
|
assert report.success
|
|
assert report["task_0"] == 0
|
|
assert report["task_1"] == 10
|
|
assert report["task_2"] == 20
|
|
assert report["gen"] == ["task_0", "task_1", "task_2"]
|
|
|
|
|
|
def test_dynamic_auto_depends_on_generator() -> None:
|
|
"""生成的 spec 自动依赖生成方,保证执行顺序。"""
|
|
order: list[str] = []
|
|
|
|
def generator() -> list[TaskSpec]:
|
|
order.append("gen")
|
|
return [TaskSpec("child", fn=lambda: order.append("child"))]
|
|
|
|
graph = Graph.from_specs([TaskSpec("gen", fn=generator, dynamic=True)])
|
|
report = px.run(graph, strategy="dependency")
|
|
assert report.success
|
|
assert order.index("gen") < order.index("child")
|
|
|
|
|
|
def test_dynamic_multi_level() -> None:
|
|
"""生成的任务本身也是 dynamic,再生成下一级。"""
|
|
|
|
def level1() -> list[TaskSpec]:
|
|
return [TaskSpec("level2", fn=level2, dynamic=True)]
|
|
|
|
def level2() -> list[TaskSpec]:
|
|
return [TaskSpec("leaf", fn=lambda: "done")]
|
|
|
|
graph = Graph.from_specs([TaskSpec("level1", fn=level1, dynamic=True)])
|
|
report = px.run(graph, strategy="dependency")
|
|
assert report.success
|
|
assert report["leaf"] == "done"
|
|
assert report["level2"] == ["leaf"]
|
|
assert report["level1"] == ["level2"]
|
|
|
|
|
|
def test_dynamic_with_existing_deps() -> None:
|
|
"""生成的 spec 可引用图中已有任务。"""
|
|
|
|
def base_task() -> int:
|
|
return 100
|
|
|
|
def generator(base_task: int) -> list[TaskSpec]:
|
|
return [TaskSpec("consumer", fn=lambda: base_task + 1)]
|
|
|
|
graph = Graph.from_specs(
|
|
[
|
|
TaskSpec("base_task", fn=base_task),
|
|
TaskSpec("gen", fn=generator, depends_on=("base_task",), dynamic=True),
|
|
]
|
|
)
|
|
report = px.run(graph, strategy="dependency")
|
|
assert report.success
|
|
assert report["consumer"] == 101
|
|
|
|
|
|
def test_dynamic_non_dependency_strategy_raises() -> None:
|
|
"""非 dependency 策略下使用 dynamic=True 抛 ValueError。"""
|
|
graph = Graph.from_specs([TaskSpec("gen", fn=lambda: None, dynamic=True)])
|
|
with pytest.raises(ValueError, match="仅支持 strategy='dependency'"):
|
|
px.run(graph, strategy="sequential")
|
|
|
|
|
|
def test_dynamic_none_return() -> None:
|
|
"""dynamic 任务返回 None 时不生成新任务。"""
|
|
|
|
def generator() -> None:
|
|
return None
|
|
|
|
graph = Graph.from_specs([TaskSpec("gen", fn=generator, dynamic=True)])
|
|
report = px.run(graph, strategy="dependency")
|
|
assert report.success
|
|
# result.value 仍为 None(无生成任务)
|
|
assert report["gen"] is None
|
|
|
|
|
|
def test_dynamic_empty_list_return() -> None:
|
|
"""dynamic 任务返回空列表时不生成新任务。"""
|
|
|
|
def generator() -> list[TaskSpec]:
|
|
return []
|
|
|
|
graph = Graph.from_specs([TaskSpec("gen", fn=generator, dynamic=True)])
|
|
report = px.run(graph, strategy="dependency")
|
|
assert report.success
|
|
assert report["gen"] == []
|
|
|
|
|
|
def test_non_dynamic_task_unaffected() -> None:
|
|
"""非动态任务的行为不受影响。"""
|
|
graph = Graph.from_specs(
|
|
[
|
|
TaskSpec("a", fn=lambda: 1),
|
|
TaskSpec("b", fn=lambda a: a + 1, depends_on=("a",)),
|
|
]
|
|
)
|
|
report = px.run(graph, strategy="dependency")
|
|
assert report.success
|
|
assert report["a"] == 1
|
|
assert report["b"] == 2
|
|
|
|
|
|
def test_dynamic_with_task_decorator() -> None:
|
|
"""@task 装饰器支持 dynamic 参数。"""
|
|
|
|
@px.task(dynamic=True)
|
|
def gen() -> list[TaskSpec]:
|
|
return [TaskSpec(f"s{i}", fn=lambda i=i: i) for i in range(3)]
|
|
|
|
graph = Graph.from_specs([gen])
|
|
report = px.run(graph, strategy="dependency")
|
|
assert report.success
|
|
assert report["s0"] == 0
|
|
assert report["s1"] == 1
|
|
assert report["s2"] == 2
|
|
|
|
|
|
def test_dynamic_spec_with_explicit_depends_on() -> None:
|
|
"""生成的 spec 已有 depends_on 时不重复添加生成方。"""
|
|
|
|
def generator() -> list[TaskSpec]:
|
|
return [TaskSpec("child", fn=lambda: 1, depends_on=("gen",))]
|
|
|
|
graph = Graph.from_specs([TaskSpec("gen", fn=generator, dynamic=True)])
|
|
report = px.run(graph, strategy="dependency")
|
|
assert report.success
|
|
# depends_on 不重复
|
|
child_spec = graph.specs["child"]
|
|
assert child_spec.depends_on.count("gen") == 1
|