"""数据流增强测试: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]