154 lines
5.1 KiB
Python
154 lines
5.1 KiB
Python
"""数据流增强测试:outputs 引用解析、pipeline 语义化管道。
|
|
|
|
覆盖:
|
|
* RunReport.output_of() 基本路径解析("$" / 单键 / 嵌套键)。
|
|
* output_of() 错误场景(任务不存在 / 未声明 outputs / output_name 未声明)。
|
|
* Graph.pipeline() 语义化管道:数据流通过上下文注入传递。
|
|
* pipeline() 与 chain() 行为一致。
|
|
* outputs 在 to_dict 序列化中保留。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
import pyflowx as px
|
|
from pyflowx import Graph, TaskSpec
|
|
|
|
|
|
def test_output_of_entire_result() -> None:
|
|
"""outputs={"value": "$"} → output_of 返回整个结果。"""
|
|
graph = Graph.from_specs([TaskSpec("a", fn=lambda: 42, outputs={"value": "$"})])
|
|
report = px.run(graph, strategy="sequential")
|
|
assert report.output_of("a", "value") == 42
|
|
|
|
|
|
def test_output_of_dict_key() -> None:
|
|
"""outputs={"path": "url"} → output_of 返回 result["url"]。"""
|
|
graph = Graph.from_specs([TaskSpec("a", fn=lambda: {"url": "http://x", "code": 200}, outputs={"path": "url"})])
|
|
report = px.run(graph, strategy="sequential")
|
|
assert report.output_of("a", "path") == "http://x"
|
|
|
|
|
|
def test_output_of_nested_path() -> None:
|
|
"""outputs={"host": "data.server.host"} → 嵌套字典路径解析。"""
|
|
graph = Graph.from_specs(
|
|
[
|
|
TaskSpec(
|
|
"a",
|
|
fn=lambda: {"data": {"server": {"host": "localhost"}}},
|
|
outputs={"host": "data.server.host"},
|
|
)
|
|
]
|
|
)
|
|
report = px.run(graph, strategy="sequential")
|
|
assert report.output_of("a", "host") == "localhost"
|
|
|
|
|
|
def test_output_of_task_not_found() -> None:
|
|
"""任务不在报告中时抛 KeyError。"""
|
|
graph = Graph.from_specs([TaskSpec("a", fn=lambda: 1)])
|
|
report = px.run(graph, strategy="sequential")
|
|
with pytest.raises(KeyError, match="不在报告中"):
|
|
report.output_of("nonexistent", "x")
|
|
|
|
|
|
def test_output_of_no_outputs_declared() -> None:
|
|
"""任务未声明 outputs 时抛 KeyError。"""
|
|
graph = Graph.from_specs([TaskSpec("a", fn=lambda: 1)])
|
|
report = px.run(graph, strategy="sequential")
|
|
with pytest.raises(KeyError, match="未声明输出"):
|
|
report.output_of("a", "x")
|
|
|
|
|
|
def test_output_of_output_name_not_declared() -> None:
|
|
"""output_name 不在 outputs 映射中时抛 KeyError。"""
|
|
graph = Graph.from_specs([TaskSpec("a", fn=lambda: 1, outputs={"x": "$"})])
|
|
report = px.run(graph, strategy="sequential")
|
|
with pytest.raises(KeyError, match="未声明输出"):
|
|
report.output_of("a", "y")
|
|
|
|
|
|
def test_pipeline_data_flow() -> None:
|
|
"""pipeline() 中前驱结果注入后继同名参数。"""
|
|
|
|
def extract() -> list[int]:
|
|
return [1, 2, 3]
|
|
|
|
def double(extract: list[int]) -> list[int]:
|
|
return [x * 2 for x in extract]
|
|
|
|
def total(double: list[int]) -> int:
|
|
return sum(double)
|
|
|
|
graph = Graph().pipeline(
|
|
TaskSpec("extract", fn=extract),
|
|
TaskSpec("double", fn=double),
|
|
TaskSpec("total", fn=total),
|
|
)
|
|
report = px.run(graph, strategy="sequential")
|
|
assert report["extract"] == [1, 2, 3]
|
|
assert report["double"] == [2, 4, 6]
|
|
assert report["total"] == 12
|
|
|
|
|
|
def test_pipeline_same_as_chain() -> None:
|
|
"""pipeline() 与 chain() 产生相同的依赖关系。"""
|
|
specs1 = [
|
|
TaskSpec("a", fn=lambda: 1),
|
|
TaskSpec("b", fn=lambda a: a + 1),
|
|
TaskSpec("c", fn=lambda b: b + 1),
|
|
]
|
|
specs2 = [
|
|
TaskSpec("a", fn=lambda: 1),
|
|
TaskSpec("b", fn=lambda a: a + 1),
|
|
TaskSpec("c", fn=lambda b: b + 1),
|
|
]
|
|
g1 = Graph().chain(*specs1)
|
|
g2 = Graph().pipeline(*specs2)
|
|
assert g1.deps == g2.deps
|
|
|
|
|
|
def test_pipeline_with_outputs() -> None:
|
|
"""pipeline() 配合 outputs 声明,通过 output_of 查询。"""
|
|
|
|
def make_data() -> dict[str, object]:
|
|
return {"items": [1, 2, 3], "count": 3}
|
|
|
|
graph = Graph().pipeline(
|
|
TaskSpec("make_data", fn=make_data, outputs={"items": "items", "count": "count"}),
|
|
)
|
|
report = px.run(graph, strategy="sequential")
|
|
assert report.output_of("make_data", "items") == [1, 2, 3]
|
|
assert report.output_of("make_data", "count") == 3
|
|
|
|
|
|
def test_pipeline_returns_self() -> None:
|
|
"""pipeline() 返回 self 支持链式调用。"""
|
|
graph = Graph()
|
|
result = graph.pipeline(TaskSpec("a", fn=lambda: 1))
|
|
assert result is graph
|
|
|
|
|
|
def test_output_of_with_strategy_dependency() -> None:
|
|
"""output_of 在 dependency 策略下也正常工作。"""
|
|
graph = Graph.from_specs(
|
|
[
|
|
TaskSpec(
|
|
"a",
|
|
fn=lambda: {"x": 10, "y": 20},
|
|
outputs={"x": "x", "y": "y"},
|
|
)
|
|
]
|
|
)
|
|
report = px.run(graph, strategy="dependency")
|
|
assert report.output_of("a", "x") == 10
|
|
assert report.output_of("a", "y") == 20
|
|
|
|
|
|
def test_output_of_list_result_with_dollar() -> None:
|
|
"""列表结果用 "$" 路径取整个值。"""
|
|
graph = Graph.from_specs([TaskSpec("a", fn=lambda: [1, 2, 3], outputs={"all": "$"})])
|
|
report = px.run(graph, strategy="sequential")
|
|
assert report.output_of("a", "all") == [1, 2, 3]
|