feat: 初始化PyFlowX轻量级DAG任务调度库
实现完整的DAG任务调度核心功能,包括: 1. 支持同步/异步/线程三种执行策略 2. 自动上下文注入,无需手动绑定任务依赖 3. 内置状态后端,支持断点续跑 4. 提供完整的测试用例与示例代码 5. 添加CI/CD配置与发布流程
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
"""Tests for Graph construction, validation, layering and subgraphs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.errors import CycleError, DuplicateTaskError, MissingDependencyError
|
||||
|
||||
|
||||
def _fn() -> None:
|
||||
return None
|
||||
|
||||
|
||||
def test_from_specs_builds_graph() -> None:
|
||||
graph = px.Graph.from_specs([
|
||||
px.TaskSpec("a", _fn),
|
||||
px.TaskSpec("b", _fn, ("a",)),
|
||||
px.TaskSpec("c", _fn, ("a", "b")),
|
||||
])
|
||||
assert set(graph.names) == {"a", "b", "c"}
|
||||
assert graph.dependencies("c") == ("a", "b")
|
||||
assert len(graph) == 3
|
||||
assert "a" in graph
|
||||
|
||||
|
||||
def test_from_specs_allows_forward_references() -> None:
|
||||
# b depends on a, but a is declared after b — order should not matter.
|
||||
graph = px.Graph.from_specs([
|
||||
px.TaskSpec("b", _fn, ("a",)),
|
||||
px.TaskSpec("a", _fn),
|
||||
])
|
||||
assert graph.layers() == [["a"], ["b"]]
|
||||
|
||||
|
||||
def test_duplicate_task_raises() -> None:
|
||||
with pytest.raises(DuplicateTaskError):
|
||||
px.Graph.from_specs([
|
||||
px.TaskSpec("a", _fn),
|
||||
px.TaskSpec("a", _fn),
|
||||
])
|
||||
|
||||
|
||||
def test_missing_dependency_raises() -> None:
|
||||
with pytest.raises(MissingDependencyError) as exc_info:
|
||||
px.Graph.from_specs([px.TaskSpec("b", _fn, ("a",))])
|
||||
assert exc_info.value.task == "b"
|
||||
assert exc_info.value.dependency == "a"
|
||||
|
||||
|
||||
def test_cycle_detection() -> None:
|
||||
with pytest.raises(CycleError):
|
||||
px.Graph.from_specs([
|
||||
px.TaskSpec("a", _fn, ("c",)),
|
||||
px.TaskSpec("b", _fn, ("a",)),
|
||||
px.TaskSpec("c", _fn, ("b",)),
|
||||
])
|
||||
|
||||
|
||||
def test_layers_grouping() -> None:
|
||||
graph = px.Graph.from_specs([
|
||||
px.TaskSpec("a", _fn),
|
||||
px.TaskSpec("b", _fn),
|
||||
px.TaskSpec("c", _fn, ("a", "b")),
|
||||
px.TaskSpec("d", _fn, ("c",)),
|
||||
])
|
||||
layers = graph.layers()
|
||||
assert layers == [["a", "b"], ["c"], ["d"]]
|
||||
|
||||
|
||||
def test_self_dependency_rejected() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
px.TaskSpec("a", _fn, ("a",))
|
||||
|
||||
|
||||
def test_to_mermaid() -> None:
|
||||
graph = px.Graph.from_specs([
|
||||
px.TaskSpec("a", _fn),
|
||||
px.TaskSpec("b", _fn, ("a",)),
|
||||
])
|
||||
mermaid = graph.to_mermaid()
|
||||
assert mermaid.startswith("graph TD")
|
||||
assert 'a["a"]' in mermaid
|
||||
assert "a --> b" in mermaid
|
||||
|
||||
|
||||
def test_to_mermaid_invalid_orientation() -> None:
|
||||
graph = px.Graph.from_specs([px.TaskSpec("a", _fn)])
|
||||
with pytest.raises(ValueError):
|
||||
graph.to_mermaid("XX")
|
||||
|
||||
|
||||
def test_subgraph_by_tags() -> None:
|
||||
graph = px.Graph.from_specs([
|
||||
px.TaskSpec("a", _fn, tags=("ingest",)),
|
||||
px.TaskSpec("b", _fn, ("a",), tags=("ingest",)),
|
||||
px.TaskSpec("c", _fn, ("b",), tags=("report",)),
|
||||
])
|
||||
sub = graph.subgraph(["ingest"])
|
||||
assert set(sub.names) == {"a", "b"}
|
||||
# Edge to dropped task c is removed; b no longer waits for anything
|
||||
# outside the subgraph (c was never a dep of b anyway).
|
||||
assert sub.dependencies("b") == ("a",)
|
||||
|
||||
|
||||
def test_subgraph_by_names() -> None:
|
||||
graph = px.Graph.from_specs([
|
||||
px.TaskSpec("a", _fn),
|
||||
px.TaskSpec("b", _fn, ("a",)),
|
||||
px.TaskSpec("c", _fn, ("b",)),
|
||||
])
|
||||
sub = graph.subgraph_by_names(["a", "b"])
|
||||
assert set(sub.names) == {"a", "b"}
|
||||
# c is dropped, so b's dep on c (none here) — but a->b edge preserved.
|
||||
assert sub.dependencies("b") == ("a",)
|
||||
|
||||
|
||||
def test_subgraph_by_names_unknown() -> None:
|
||||
graph = px.Graph.from_specs([px.TaskSpec("a", _fn)])
|
||||
with pytest.raises(KeyError):
|
||||
graph.subgraph_by_names(["nope"])
|
||||
|
||||
|
||||
def test_describe() -> None:
|
||||
graph = px.Graph.from_specs([
|
||||
px.TaskSpec("a", _fn),
|
||||
px.TaskSpec("b", _fn, ("a",)),
|
||||
])
|
||||
desc = graph.describe()
|
||||
assert "Layer 1" in desc
|
||||
assert "Layer 2" in desc
|
||||
Reference in New Issue
Block a user