Files
pyflowx/tests/test_fileops.py
T

686 lines
26 KiB
Python

"""fileops 底层文件 API 测试.
覆盖 4 类能力(查找遍历/读写/复制移动删除/元数据校验)+
DAG 组合调用示例(用 px.TaskSpec + outputs + output_of 验证数据流).
Mock 优先级:用 tmp_path 创建真实文件, 不用 mock(标准库文件操作可控).
"""
from __future__ import annotations
from pathlib import Path
import pytest
import pyflowx as px
from pyflowx.fileops import (
append_text,
copy,
copy_tree,
delete,
exists,
find,
glob,
is_dir,
is_file,
move,
mtime,
read_bytes,
read_text,
sha256,
size,
stem,
suffix,
walk_files,
write_bytes,
write_text,
)
# ====================================================================== #
# 查找与遍历
# ====================================================================== #
class TestFind:
"""find() 查找匹配."""
def test_find_recursive_all(self, tmp_path: Path) -> None:
"""递归查找所有路径."""
(tmp_path / "a.txt").write_text("a")
(tmp_path / "sub").mkdir()
(tmp_path / "sub" / "b.py").write_text("b")
result = find(tmp_path, "*")
names = sorted(p.name for p in result)
assert "a.txt" in names
assert "b.py" in names
assert "sub" in names
def test_find_by_pattern(self, tmp_path: Path) -> None:
"""按扩展名过滤."""
(tmp_path / "a.py").write_text("")
(tmp_path / "b.txt").write_text("")
(tmp_path / "c.py").write_text("")
result = find(tmp_path, "*.py")
assert sorted(p.name for p in result) == ["a.py", "c.py"]
def test_find_non_recursive(self, tmp_path: Path) -> None:
"""recursive=False 仅顶层."""
(tmp_path / "a.py").write_text("")
(tmp_path / "sub").mkdir()
(tmp_path / "sub" / "b.py").write_text("")
result = find(tmp_path, "*.py", recursive=False)
assert sorted(p.name for p in result) == ["a.py"]
def test_find_max_depth(self, tmp_path: Path) -> None:
"""max_depth 限制递归深度."""
(tmp_path / "l0.txt").write_text("")
(tmp_path / "d1").mkdir()
(tmp_path / "d1" / "l1.txt").write_text("")
(tmp_path / "d1" / "d2").mkdir()
(tmp_path / "d1" / "d2" / "l2.txt").write_text("")
result = find(tmp_path, "*.txt", max_depth=1)
names = sorted(p.name for p in result)
assert "l0.txt" in names
assert "l1.txt" in names
assert "l2.txt" not in names
def test_find_max_depth_zero(self, tmp_path: Path) -> None:
"""max_depth=0 仅 root 直接 glob."""
(tmp_path / "l0.txt").write_text("")
(tmp_path / "d1").mkdir()
(tmp_path / "d1" / "l1.txt").write_text("")
result = find(tmp_path, "*.txt", max_depth=0)
names = [p.name for p in result]
assert names == ["l0.txt"]
def test_find_only_files(self, tmp_path: Path) -> None:
"""only_files 仅返回文件."""
(tmp_path / "a.txt").write_text("")
(tmp_path / "sub").mkdir()
result = find(tmp_path, "*", only_files=True)
assert all(p.is_file() for p in result)
assert not any(p.is_dir() for p in result)
def test_find_only_dirs(self, tmp_path: Path) -> None:
"""only_dirs 仅返回目录."""
(tmp_path / "a.txt").write_text("")
(tmp_path / "sub").mkdir()
result = find(tmp_path, "*", only_dirs=True)
assert all(p.is_dir() for p in result)
def test_find_only_files_and_only_dirs_returns_empty(self, tmp_path: Path) -> None:
"""only_files + only_dirs 同时 True 返回空列表."""
(tmp_path / "a.txt").write_text("")
result = find(tmp_path, "*", only_files=True, only_dirs=True)
assert result == []
def test_find_root_not_exist(self) -> None:
"""根目录不存在抛 FileNotFoundError."""
with pytest.raises(FileNotFoundError, match="查找根目录"):
find("/nonexistent/path/xyz", "*")
def test_find_returns_sorted(self, tmp_path: Path) -> None:
"""结果按路径字典序排序."""
(tmp_path / "c.txt").write_text("")
(tmp_path / "a.txt").write_text("")
(tmp_path / "b.txt").write_text("")
result = find(tmp_path, "*.txt", recursive=False)
assert [p.name for p in result] == ["a.txt", "b.txt", "c.txt"]
class TestGlob:
"""glob() 简化接口."""
def test_glob_with_pattern(self, tmp_path: Path) -> None:
"""glob 模式匹配."""
(tmp_path / "a.py").write_text("")
(tmp_path / "b.txt").write_text("")
result = glob("*.py", root=tmp_path)
assert [p.name for p in result] == ["a.py"]
def test_glob_default_root(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""root=None 用当前目录."""
monkeypatch.chdir(tmp_path)
(tmp_path / "x.txt").write_text("")
result = glob("*.txt")
assert [p.name for p in result] == ["x.txt"]
def test_glob_recursive_double_star(self, tmp_path: Path) -> None:
"""** 递归匹配."""
(tmp_path / "a.py").write_text("")
(tmp_path / "sub").mkdir()
(tmp_path / "sub" / "b.py").write_text("")
result = glob("**/*.py", root=tmp_path)
names = sorted(p.name for p in result)
assert "a.py" in names
assert "b.py" in names
class TestWalkFiles:
"""walk_files() 生成器遍历."""
def test_walk_files_yields_files_only(self, tmp_path: Path) -> None:
"""仅产生文件, 不产生目录."""
(tmp_path / "a.txt").write_text("")
(tmp_path / "sub").mkdir()
(tmp_path / "sub" / "b.txt").write_text("")
result = list(walk_files(tmp_path))
assert all(p.is_file() for p in result)
names = sorted(p.name for p in result)
assert names == ["a.txt", "b.txt"]
def test_walk_files_max_depth(self, tmp_path: Path) -> None:
"""max_depth 限制."""
(tmp_path / "l0.txt").write_text("")
(tmp_path / "d1").mkdir()
(tmp_path / "d1" / "l1.txt").write_text("")
(tmp_path / "d1" / "d2").mkdir()
(tmp_path / "d1" / "d2" / "l2.txt").write_text("")
result = list(walk_files(tmp_path, max_depth=0))
assert [p.name for p in result] == ["l0.txt"]
def test_walk_files_root_not_exist(self) -> None:
"""根目录不存在抛 FileNotFoundError."""
with pytest.raises(FileNotFoundError, match="遍历根目录"):
list(walk_files("/nonexistent/xyz"))
def test_walk_files_lazy(self, tmp_path: Path) -> None:
"""返回迭代器, 惰性求值."""
(tmp_path / "a.txt").write_text("")
result = walk_files(tmp_path)
assert hasattr(result, "__next__")
first = next(result)
assert first.is_file()
# ====================================================================== #
# 读写
# ====================================================================== #
class TestReadText:
def test_read_text(self, tmp_path: Path) -> None:
"""读取文本."""
(tmp_path / "a.txt").write_text("hello", encoding="utf-8")
assert read_text(tmp_path / "a.txt") == "hello"
def test_read_text_encoding(self, tmp_path: Path) -> None:
"""指定编码."""
(tmp_path / "a.txt").write_text("你好", encoding="gbk")
assert read_text(tmp_path / "a.txt", encoding="gbk") == "你好"
def test_read_text_not_exist(self, tmp_path: Path) -> None:
"""文件不存在抛 FileNotFoundError."""
with pytest.raises(FileNotFoundError):
read_text(tmp_path / "nonexistent.txt")
class TestReadBytes:
def test_read_bytes(self, tmp_path: Path) -> None:
"""读取字节."""
(tmp_path / "a.bin").write_bytes(b"\x00\x01\x02")
assert read_bytes(tmp_path / "a.bin") == b"\x00\x01\x02"
def test_read_bytes_not_exist(self, tmp_path: Path) -> None:
with pytest.raises(FileNotFoundError):
read_bytes(tmp_path / "nonexistent.bin")
class TestWriteText:
def test_write_text_returns_byte_count(self, tmp_path: Path) -> None:
"""返回写入字节数."""
n = write_text(tmp_path / "a.txt", "hello")
assert n == 5
def test_write_text_creates_parent(self, tmp_path: Path) -> None:
"""父目录不存在自动创建."""
target = tmp_path / "sub" / "deep" / "a.txt"
write_text(target, "x")
assert target.read_text() == "x"
def test_write_text_encoding(self, tmp_path: Path) -> None:
"""指定编码写入."""
write_text(tmp_path / "a.txt", "你好", encoding="gbk")
assert (tmp_path / "a.txt").read_bytes() == "你好".encode("gbk")
class TestWriteBytes:
def test_write_bytes_returns_length(self, tmp_path: Path) -> None:
n = write_bytes(tmp_path / "a.bin", b"\x00\x01")
assert n == 2
def test_write_bytes_creates_parent(self, tmp_path: Path) -> None:
target = tmp_path / "sub" / "a.bin"
write_bytes(target, b"data")
assert target.read_bytes() == b"data"
class TestAppendText:
def test_append_to_existing(self, tmp_path: Path) -> None:
"""追加到已有文件."""
(tmp_path / "a.txt").write_text("hello")
append_text(tmp_path / "a.txt", " world")
assert (tmp_path / "a.txt").read_text() == "hello world"
def test_append_creates_new(self, tmp_path: Path) -> None:
"""文件不存在时创建."""
append_text(tmp_path / "a.txt", "first")
assert (tmp_path / "a.txt").read_text() == "first"
def test_append_creates_parent(self, tmp_path: Path) -> None:
"""父目录不存在自动创建."""
append_text(tmp_path / "sub" / "a.txt", "x")
assert (tmp_path / "sub" / "a.txt").read_text() == "x"
def test_append_returns_byte_count(self, tmp_path: Path) -> None:
n = append_text(tmp_path / "a.txt", "hello")
assert n == 5
# ====================================================================== #
# 复制 / 移动 / 删除
# ====================================================================== #
class TestCopy:
def test_copy_file(self, tmp_path: Path) -> None:
"""复制文件."""
(tmp_path / "a.txt").write_text("hello")
dst = copy(tmp_path / "a.txt", tmp_path / "b.txt")
assert dst == tmp_path / "b.txt"
assert (tmp_path / "b.txt").read_text() == "hello"
assert (tmp_path / "a.txt").exists()
def test_copy_to_dir(self, tmp_path: Path) -> None:
"""目标是目录时复制到目录内."""
(tmp_path / "a.txt").write_text("x")
(tmp_path / "dst").mkdir()
dst = copy(tmp_path / "a.txt", tmp_path / "dst")
assert dst == tmp_path / "dst" / "a.txt"
assert dst.read_text() == "x"
def test_copy_dir_recursive(self, tmp_path: Path) -> None:
"""复制目录树."""
(tmp_path / "src").mkdir()
(tmp_path / "src" / "a.txt").write_text("a")
(tmp_path / "src" / "sub").mkdir()
(tmp_path / "src" / "sub" / "b.txt").write_text("b")
dst = copy(tmp_path / "src", tmp_path / "dst")
assert (dst / "a.txt").read_text() == "a"
assert (dst / "sub" / "b.txt").read_text() == "b"
def test_copy_no_overwrite(self, tmp_path: Path) -> None:
"""overwrite=False 时目标存在抛错."""
(tmp_path / "a.txt").write_text("original")
(tmp_path / "b.txt").write_text("exists")
with pytest.raises(FileExistsError, match="overwrite=False"):
copy(tmp_path / "a.txt", tmp_path / "b.txt")
def test_copy_overwrite(self, tmp_path: Path) -> None:
"""overwrite=True 覆盖."""
(tmp_path / "a.txt").write_text("new")
(tmp_path / "b.txt").write_text("old")
copy(tmp_path / "a.txt", tmp_path / "b.txt", overwrite=True)
assert (tmp_path / "b.txt").read_text() == "new"
def test_copy_dir_overwrite_existing_dir(self, tmp_path: Path) -> None:
"""overwrite=True 覆盖已存在目录."""
(tmp_path / "src").mkdir()
(tmp_path / "src" / "new.txt").write_text("new")
(tmp_path / "dst").mkdir()
(tmp_path / "dst" / "old.txt").write_text("old")
copy(tmp_path / "src", tmp_path / "dst", overwrite=True)
assert (tmp_path / "dst" / "new.txt").read_text() == "new"
assert not (tmp_path / "dst" / "old.txt").exists()
def test_copy_src_not_exist(self, tmp_path: Path) -> None:
with pytest.raises(FileNotFoundError, match="源路径不存在"):
copy(tmp_path / "nonexistent", tmp_path / "dst")
def test_copy_creates_parent(self, tmp_path: Path) -> None:
"""目标父目录自动创建."""
(tmp_path / "a.txt").write_text("x")
copy(tmp_path / "a.txt", tmp_path / "sub" / "deep" / "b.txt")
assert (tmp_path / "sub" / "deep" / "b.txt").read_text() == "x"
class TestMove:
def test_move_file(self, tmp_path: Path) -> None:
(tmp_path / "a.txt").write_text("hello")
dst = move(tmp_path / "a.txt", tmp_path / "b.txt")
assert dst == tmp_path / "b.txt"
assert (tmp_path / "b.txt").read_text() == "hello"
assert not (tmp_path / "a.txt").exists()
def test_move_to_dir(self, tmp_path: Path) -> None:
(tmp_path / "a.txt").write_text("x")
(tmp_path / "dst").mkdir()
dst = move(tmp_path / "a.txt", tmp_path / "dst")
assert dst == tmp_path / "dst" / "a.txt"
assert not (tmp_path / "a.txt").exists()
def test_move_no_overwrite(self, tmp_path: Path) -> None:
(tmp_path / "a.txt").write_text("x")
(tmp_path / "b.txt").write_text("y")
with pytest.raises(FileExistsError, match="overwrite=False"):
move(tmp_path / "a.txt", tmp_path / "b.txt")
def test_move_overwrite(self, tmp_path: Path) -> None:
(tmp_path / "a.txt").write_text("new")
(tmp_path / "b.txt").write_text("old")
move(tmp_path / "a.txt", tmp_path / "b.txt", overwrite=True)
assert (tmp_path / "b.txt").read_text() == "new"
assert not (tmp_path / "a.txt").exists()
def test_move_overwrite_dir(self, tmp_path: Path) -> None:
"""overwrite=True 覆盖目录."""
(tmp_path / "src").mkdir()
(tmp_path / "src" / "a.txt").write_text("new")
(tmp_path / "dst").mkdir()
(tmp_path / "dst" / "old.txt").write_text("old")
move(tmp_path / "src", tmp_path / "dst", overwrite=True)
assert (tmp_path / "dst" / "a.txt").read_text() == "new"
assert not (tmp_path / "dst" / "old.txt").exists()
def test_move_src_not_exist(self, tmp_path: Path) -> None:
with pytest.raises(FileNotFoundError, match="源路径不存在"):
move(tmp_path / "nonexistent", tmp_path / "dst")
class TestDelete:
def test_delete_file(self, tmp_path: Path) -> None:
(tmp_path / "a.txt").write_text("x")
assert delete(tmp_path / "a.txt") is True
assert not (tmp_path / "a.txt").exists()
def test_delete_dir(self, tmp_path: Path) -> None:
(tmp_path / "d").mkdir()
(tmp_path / "d" / "a.txt").write_text("x")
assert delete(tmp_path / "d") is True
assert not (tmp_path / "d").exists()
def test_delete_missing_ok_true(self, tmp_path: Path) -> None:
"""missing_ok=True 路径不存在返回 False."""
assert delete(tmp_path / "nonexistent", missing_ok=True) is False
def test_delete_missing_ok_false(self, tmp_path: Path) -> None:
"""missing_ok=False 路径不存在抛错."""
with pytest.raises(FileNotFoundError, match="路径不存在"):
delete(tmp_path / "nonexistent")
class TestCopyTree:
def test_copy_tree_returns_file_list(self, tmp_path: Path) -> None:
"""返回已复制文件列表."""
(tmp_path / "src").mkdir()
(tmp_path / "src" / "a.txt").write_text("a")
(tmp_path / "src" / "sub").mkdir()
(tmp_path / "src" / "sub" / "b.txt").write_text("b")
result = copy_tree(tmp_path / "src", tmp_path / "dst")
names = sorted(p.name for p in result)
assert names == ["a.txt", "b.txt"]
assert all(p.is_file() for p in result)
def test_copy_tree_no_overwrite(self, tmp_path: Path) -> None:
(tmp_path / "src").mkdir()
(tmp_path / "src" / "a.txt").write_text("a")
(tmp_path / "dst").mkdir()
with pytest.raises(FileExistsError, match="overwrite=False"):
copy_tree(tmp_path / "src", tmp_path / "dst")
def test_copy_tree_overwrite(self, tmp_path: Path) -> None:
(tmp_path / "src").mkdir()
(tmp_path / "src" / "a.txt").write_text("new")
(tmp_path / "dst").mkdir()
(tmp_path / "dst" / "old.txt").write_text("old")
copy_tree(tmp_path / "src", tmp_path / "dst", overwrite=True)
assert (tmp_path / "dst" / "a.txt").read_text() == "new"
assert not (tmp_path / "dst" / "old.txt").exists()
def test_copy_tree_src_not_dir(self, tmp_path: Path) -> None:
(tmp_path / "a.txt").write_text("x")
with pytest.raises(FileNotFoundError, match="源目录不存在或非目录"):
copy_tree(tmp_path / "a.txt", tmp_path / "dst")
# ====================================================================== #
# 元数据与校验
# ====================================================================== #
class TestSize:
def test_size(self, tmp_path: Path) -> None:
(tmp_path / "a.txt").write_bytes(b"hello")
assert size(tmp_path / "a.txt") == 5
def test_size_not_exist(self, tmp_path: Path) -> None:
with pytest.raises(FileNotFoundError):
size(tmp_path / "nonexistent")
class TestMtime:
def test_mtime(self, tmp_path: Path) -> None:
(tmp_path / "a.txt").write_text("x")
t = mtime(tmp_path / "a.txt")
assert isinstance(t, float)
assert t > 0
def test_mtime_not_exist(self, tmp_path: Path) -> None:
with pytest.raises(FileNotFoundError):
mtime(tmp_path / "nonexistent")
class TestExistsIsFileIsDir:
def test_exists_true(self, tmp_path: Path) -> None:
(tmp_path / "a.txt").write_text("x")
assert exists(tmp_path / "a.txt") is True
def test_exists_false(self, tmp_path: Path) -> None:
assert exists(tmp_path / "nonexistent") is False
def test_is_file_true(self, tmp_path: Path) -> None:
(tmp_path / "a.txt").write_text("x")
assert is_file(tmp_path / "a.txt") is True
def test_is_file_false_for_dir(self, tmp_path: Path) -> None:
(tmp_path / "d").mkdir()
assert is_file(tmp_path / "d") is False
def test_is_file_false_for_nonexistent(self, tmp_path: Path) -> None:
assert is_file(tmp_path / "nonexistent") is False
def test_is_dir_true(self, tmp_path: Path) -> None:
(tmp_path / "d").mkdir()
assert is_dir(tmp_path / "d") is True
def test_is_dir_false_for_file(self, tmp_path: Path) -> None:
(tmp_path / "a.txt").write_text("x")
assert is_dir(tmp_path / "a.txt") is False
def test_is_dir_false_for_nonexistent(self, tmp_path: Path) -> None:
assert is_dir(tmp_path / "nonexistent") is False
class TestSha256:
def test_sha256_known(self, tmp_path: Path) -> None:
"""已知内容哈希."""
import hashlib
(tmp_path / "a.txt").write_bytes(b"hello")
expected = hashlib.sha256(b"hello").hexdigest()
assert sha256(tmp_path / "a.txt") == expected
def test_sha256_empty(self, tmp_path: Path) -> None:
import hashlib
(tmp_path / "empty").write_bytes(b"")
assert sha256(tmp_path / "empty") == hashlib.sha256(b"").hexdigest()
def test_sha256_chunk_size(self, tmp_path: Path) -> None:
"""小 chunk_size 也能正确计算."""
(tmp_path / "a.txt").write_bytes(b"hello world")
h1 = sha256(tmp_path / "a.txt", chunk_size=2)
h2 = sha256(tmp_path / "a.txt", chunk_size=1024)
assert h1 == h2
def test_sha256_not_exist(self, tmp_path: Path) -> None:
with pytest.raises(FileNotFoundError):
sha256(tmp_path / "nonexistent")
class TestStemSuffix:
def test_stem_simple(self) -> None:
assert stem("a.txt") == "a"
def test_stem_multiple_dots(self) -> None:
assert stem("archive.tar.gz") == "archive.tar"
def test_stem_no_extension(self) -> None:
assert stem("README") == "README"
def test_suffix_simple(self) -> None:
assert suffix("a.txt") == ".txt"
def test_suffix_multiple_dots(self) -> None:
assert suffix("archive.tar.gz") == ".gz"
def test_suffix_no_extension(self) -> None:
assert suffix("README") == ""
# ====================================================================== #
# DAG 组合调用示例
# ====================================================================== #
class TestDagComposition:
"""fileops 函数在 DAG 中组合调用, 通过 outputs + output_of 传递数据流."""
def test_find_read_pipeline(self, tmp_path: Path) -> None:
"""find → read_text 数据流: 上游查找返回路径列表, 下游读取内容.
验证:
- find 任务 outputs 声明 "paths": "$"
- 下游任务通过参数名 find_logs 注入上游结果
- report.output_of 可提取上游输出
"""
(tmp_path / "a.log").write_text("content-a")
(tmp_path / "b.log").write_text("content-b")
(tmp_path / "c.txt").write_text("ignored")
def find_logs() -> list[Path]:
return find(tmp_path, "*.log")
def read_all(find_logs: list[Path]) -> list[str]:
return [read_text(p) for p in find_logs]
graph = px.Graph.from_specs(
[
px.TaskSpec("find_logs", fn=find_logs, outputs={"paths": "$"}),
px.TaskSpec("read_all", fn=read_all, depends_on=("find_logs",)),
]
)
report = px.run(graph, strategy="sequential")
assert report.success
paths = report.output_of("find_logs", "paths")
assert len(paths) == 2
contents = report["read_all"]
assert sorted(contents) == ["content-a", "content-b"]
def test_find_copy_batch(self, tmp_path: Path) -> None:
"""find → copy 批量复制: 上游查找, 下游批量复制到目标目录."""
(tmp_path / "src").mkdir()
(tmp_path / "src" / "a.txt").write_text("a")
(tmp_path / "src" / "b.txt").write_text("b")
(tmp_path / "dst").mkdir()
def find_sources() -> list[Path]:
return find(tmp_path / "src", "*.txt")
def copy_all(find_sources: list[Path]) -> list[Path]:
return [copy(p, tmp_path / "dst") for p in find_sources]
graph = px.Graph.from_specs(
[
px.TaskSpec("find_sources", fn=find_sources, outputs={"files": "$"}),
px.TaskSpec("copy_all", fn=copy_all, depends_on=("find_sources",)),
]
)
report = px.run(graph, strategy="sequential")
assert report.success
copied = report["copy_all"]
assert len(copied) == 2
dst_files = sorted(p.name for p in (tmp_path / "dst").iterdir())
assert dst_files == ["a.txt", "b.txt"]
def test_find_sha256_checksum(self, tmp_path: Path) -> None:
"""find → sha256 校验: 上游查找, 下游计算每个文件哈希."""
(tmp_path / "a.bin").write_bytes(b"hello")
(tmp_path / "b.bin").write_bytes(b"world")
import hashlib
def find_bins() -> list[Path]:
return find(tmp_path, "*.bin")
def checksums(find_bins: list[Path]) -> dict[str, str]:
return {p.name: sha256(p) for p in find_bins}
graph = px.Graph.from_specs(
[
px.TaskSpec("find_bins", fn=find_bins, outputs={"paths": "$"}),
px.TaskSpec("checksums", fn=checksums, depends_on=("find_bins",)),
]
)
report = px.run(graph, strategy="sequential")
assert report.success
result = report["checksums"]
assert result["a.bin"] == hashlib.sha256(b"hello").hexdigest()
assert result["b.bin"] == hashlib.sha256(b"world").hexdigest()
def test_write_read_roundtrip_in_dag(self, tmp_path: Path) -> None:
"""write_text → read_text 往返: 验证写后读数据流."""
target = tmp_path / "out.txt"
def write_data() -> int:
return write_text(target, "dag-content")
def read_back(write_data: int) -> str:
assert write_data == len("dag-content")
return read_text(target)
graph = px.Graph.from_specs(
[
px.TaskSpec("write_data", fn=write_data, outputs={"bytes": "$"}),
px.TaskSpec("read_back", fn=read_back, depends_on=("write_data",)),
]
)
report = px.run(graph, strategy="sequential")
assert report.success
assert report["read_back"] == "dag-content"
assert report.output_of("write_data", "bytes") == len("dag-content")
def test_three_stage_pipeline(self, tmp_path: Path) -> None:
"""三阶段流水线: find → filter → write_summary."""
(tmp_path / "data").mkdir()
for i in range(5):
(tmp_path / "data" / f"f{i}.txt").write_text(f"content-{i}")
def find_files() -> list[Path]:
return find(tmp_path / "data", "*.txt")
def summarize(find_files: list[Path]) -> dict[str, int]:
return {"count": len(find_files), "total_size": sum(size(p) for p in find_files)}
def write_summary(summarize: dict[str, int]) -> int:
content = f"files: {summarize['count']}, size: {summarize['total_size']}"
return write_text(tmp_path / "summary.txt", content)
graph = px.Graph.from_specs(
[
px.TaskSpec("find_files", fn=find_files, outputs={"paths": "$"}),
px.TaskSpec("summarize", fn=summarize, depends_on=("find_files",)),
px.TaskSpec("write_summary", fn=write_summary, depends_on=("summarize",)),
]
)
report = px.run(graph, strategy="sequential")
assert report.success
summary = read_text(tmp_path / "summary.txt")
assert "files: 5" in summary