Files

634 lines
23 KiB
Python

"""transfer.py 核心逻辑测试:解析器 / 日志轮转 / 参数解析 / 后端命令构建"""
import re
import os
import subprocess
import time
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import transfer
# ---------- 解析器: rsync ----------
def test_rsync_progress2_line():
line = " 31,457,280 12% 356.77MB/s 0:00:00 (xfr#1, to-chk=51/53)"
s = transfer.RsyncParser.feed_progress2(line)
assert s is not None
assert s.pct == 12
assert s.bytes_done == 31457280
assert s.files_done == 1
assert s.files_total == 53
assert s.speed_bps > 0
def test_rsync_progress2_complete():
line = " 243,401,728 100% 305.39MB/s 0:00:00 (xfr#52, to-chk=0/53)"
s = transfer.RsyncParser.feed_progress2(line)
assert s is not None
assert s.pct == 100
assert s.files_done == 52
assert s.files_total == 53
def test_rsync_progress2_unparseable_returns_none():
assert transfer.RsyncParser.feed_progress2("building file list") is None
assert transfer.RsyncParser.feed_progress2("") is None
def test_rsync_single_file_line():
line = "file_1.bin 31,457,280 100% 356.77MB/s 0:00:00"
f = transfer.RsyncParser.feed_file(line)
assert f is not None
assert f.name == "file_1.bin"
assert f.pct == 100
def test_rsync_error_detection():
assert transfer.RsyncParser.is_error("rsync: [sender] write error: Broken pipe (32)")
assert transfer.RsyncParser.is_error("rsync error: some files/attrs were not transferred (code 23)")
assert not transfer.RsyncParser.is_error("file_1.bin 100%")
assert not transfer.RsyncParser.is_error("")
# ---------- 解析器: rclone ----------
def test_rclone_file_line():
line = " * file_7.bin: 98% / 7.023 MiB, 2.715 MiB/s"
f = transfer.RcloneParser.feed_file(line)
assert f is not None
assert f.name == "file_7.bin"
assert f.pct == 98
def test_rclone_total_line():
line = "Transferred: 232.126 MiB / 232.126 MiB, 100%, 7.997 MiB/s, ETA 0s"
s = transfer.RcloneParser.feed_total(line)
assert s is not None
assert s.pct == 100
def test_rclone_files_count_line():
line = "Transferred: 52 / 52, 100%"
s = transfer.RcloneParser.feed_files(line)
assert s is not None
assert s.files_done == 52
assert s.files_total == 52
# ---------- 日志轮转 ----------
def test_logmanager_creates_and_appends(tmp_path):
lm = transfer.LogManager(tmp_path, keep=20)
path = lm.open_session("test")
assert os.path.exists(path)
lm.write("hello")
lm.write("world")
content = open(path).read()
assert "hello" in content
assert "world" in content
assert content.index("hello") < content.index("world") # 追加顺序正确
def test_logmanager_keeps_only_latest_20(tmp_path):
lm = transfer.LogManager(tmp_path, keep=20)
for i in range(25):
lm.open_session(f"run{i}")
lm.write(f"data{i}")
lm.prune()
files = sorted(f for f in os.listdir(tmp_path) if f.endswith(".log"))
assert len(files) == 20
run_nums = [int(re.search(r"run(\d+)", f).group(1)) for f in files]
assert 5 in run_nums
assert 4 not in run_nums
def test_logmanager_filenames_sorted_chronologically(tmp_path):
lm = transfer.LogManager(tmp_path, keep=20)
p1 = lm.open_session("a")
p2 = lm.open_session("b")
assert p1 != p2
assert p2 > p1 # 时间戳递增
# ---------- 参数解析 ----------
def test_parse_args_with_positional():
args = transfer.parse_args(["src", "dst"])
assert args.source == "src"
assert args.dest == "dst"
assert args.backend == "rsync"
assert args.keep_logs == 20
assert args.mode == "tui" # 未显式指定 cli 时默认 TUI
def test_parse_args_explicit_cli():
args = transfer.parse_args(["--mode", "cli", "src", "dst"])
assert args.mode == "cli"
def test_parse_args_no_positional_goes_tui():
args = transfer.parse_args([])
assert args.mode == "tui"
assert args.source is None
def test_parse_args_full_options():
args = transfer.parse_args([
"--backend", "rclone", "--bwlimit", "30M", "--resume",
"--checksum", "--mode", "cli", "--keep-logs", "5",
"--logdir", "/tmp/x", "src", "dst",
])
assert args.backend == "rclone"
assert args.bwlimit == "30M"
assert args.resume is True
assert args.checksum is True
assert args.keep_logs == 5
assert args.logdir == "/tmp/x"
# ---------- 后端命令构建 ----------
def test_rsync_backend_cmd():
cmd = transfer.RsyncBackend.build_cmd("a/", "b/", bwlimit="30000", resume=True, checksum=False)
assert cmd[0] == "rsync"
joined = " ".join(cmd)
assert "--info=progress2" in joined
assert "--bwlimit=30000" in joined
assert "--partial" in joined
assert "--append" in joined
assert "-a" in joined
def test_rsync_backend_checksum_flag():
cmd = transfer.RsyncBackend.build_cmd("a/", "b/", bwlimit="", resume=False, checksum=True)
assert "--checksum" in " ".join(cmd)
def test_rclone_backend_cmd():
cmd = transfer.RcloneBackend.build_cmd("a/", "b/", bwlimit="8M", transfers=4)
assert cmd[0] == "rclone"
joined = " ".join(cmd)
assert "--bwlimit" in joined
assert "--transfers" in joined
assert "--progress" in joined
def test_tar_pv_backend_cmd():
cmd = transfer.TarPvBackend.build_cmd("a/", "b/")
assert "tar" in cmd
assert "pv" in cmd
assert "|" in cmd # 管道串接
# ---------- 冲突策略: 参数 ----------
def test_policy_default_overwrite():
args = transfer.parse_args(["src", "dst"])
assert args.policy == "overwrite"
def test_policy_skip_flag():
args = transfer.parse_args(["--policy", "skip", "src", "dst"])
assert args.policy == "skip"
def test_policy_ask_flag():
args = transfer.parse_args(["--policy", "ask", "src", "dst"])
assert args.policy == "ask"
def test_policy_synonyms():
assert transfer.parse_args(["--overwrite", "s", "d"]).policy == "overwrite"
assert transfer.parse_args(["--skip", "s", "d"]).policy == "skip"
assert transfer.parse_args(["--ask", "s", "d"]).policy == "ask"
# ---------- 冲突策略: 扫描解析 ----------
def test_scan_rsync_new_file():
conflicts = transfer.ConflictScanner.parse_rsync_line(">f+++++++++|4|new.txt")
assert conflicts == ("new.txt", "new")
def test_scan_rsync_update():
conflicts = transfer.ConflictScanner.parse_rsync_line(">f.st......|123|f.txt")
assert conflicts == ("f.txt", "update")
def test_scan_rsync_metadata_only_update():
conflicts = transfer.ConflictScanner.parse_rsync_line(">f..t......|0|same.txt")
assert conflicts == ("same.txt", "update")
def test_scan_rsync_ignore_non_file_lines():
assert transfer.ConflictScanner.parse_rsync_line("cd+++++++++|0|dir/") is None
assert transfer.ConflictScanner.parse_rsync_line("*deleting |0|old.txt") is None
assert transfer.ConflictScanner.parse_rsync_line("") is None
# ---------- 冲突策略: 决策 ----------
def test_apply_policy_overwrite():
conflicts = [("a.txt", "update"), ("b.txt", "update")]
skipped = transfer.apply_policy(conflicts, "overwrite", {})
assert skipped == []
def test_apply_policy_skip():
conflicts = [("a.txt", "update"), ("b.txt", "update")]
skipped = transfer.apply_policy(conflicts, "skip", {})
assert set(skipped) == {"a.txt", "b.txt"}
def test_apply_policy_ask_mixed_answers():
conflicts = [("a.txt", "update"), ("b.txt", "update")]
answers = {"a.txt": "y", "b.txt": "n"}
skipped = transfer.apply_policy(conflicts, "ask", answers)
assert skipped == ["b.txt"] # 仅跳过选 n 的
def test_apply_policy_ask_all_overwrite():
conflicts = [("a.txt", "update")]
skipped = transfer.apply_policy(conflicts, "ask", {"*": "a"})
assert skipped == []
def test_apply_policy_ask_all_skip():
conflicts = [("a.txt", "update"), ("b.txt", "update")]
skipped = transfer.apply_policy(conflicts, "ask", {"*": "s"})
assert set(skipped) == {"a.txt", "b.txt"}
# ---------- 后端 exclude 集成 ----------
def test_rsync_backend_exclude_from():
cmd = transfer.RsyncBackend.build_cmd("a/", "b/", exclude_from="/tmp/excl.txt")
assert "--exclude-from=/tmp/excl.txt" in cmd
def test_rclone_backend_skip_policy():
cmd = transfer.RcloneBackend.build_cmd("a/", "b/", skip_existing=True)
assert "--ignore-existing" in " ".join(cmd)
def test_rsync_backend_no_append_when_overwriting():
"""覆盖场景禁用 --append (追加模式会阻止重写)"""
cmd = transfer.RsyncBackend.build_cmd("a/", "b/", resume=True, append=False)
assert "--append" not in cmd
assert "--partial" in cmd
def test_apply_policy_global_skip_keeps_explicit_yes():
"""先选 y 再选 s: 已确认覆盖的保留, 其余跳过"""
conflicts = [("a.txt", "update"), ("b.txt", "update")]
answers = {"a.txt": "y", "*": "s"}
skipped = transfer.apply_policy(conflicts, "ask", answers)
assert skipped == ["b.txt"]
def test_apply_policy_global_overwrite_keeps_explicit_no():
"""先选 n 再选 a: 已确认跳过的保留, 其余覆盖"""
conflicts = [("a.txt", "update"), ("b.txt", "update")]
answers = {"a.txt": "n", "*": "a"}
skipped = transfer.apply_policy(conflicts, "ask", answers)
assert skipped == ["a.txt"]
# ---------- 速度统计 ----------
def test_speed_stats_basic():
h = [10 * 1024 * 1024, 20 * 1024 * 1024, 15 * 1024 * 1024]
cur, peak, avg = transfer.speed_stats(h)
assert cur == 15 * 1024 * 1024
assert peak == 20 * 1024 * 1024
assert avg == 15 * 1024 * 1024
def test_speed_stats_empty():
assert transfer.speed_stats([]) == (0.0, 0.0, 0.0)
def test_speed_stats_single():
cur, peak, avg = transfer.speed_stats([8 * 1024 * 1024])
assert (cur, peak, avg) == (8 * 1024 * 1024, 8 * 1024 * 1024, 8 * 1024 * 1024)
# ---------- 目录大小轮询 (rsync 输出缓冲替代方案) ----------
def test_dir_size_returns_bytes(tmp_path):
"""du 目录返回字节数"""
(tmp_path / "a.bin").write_bytes(b"x" * 1000)
(tmp_path / "sub").mkdir()
(tmp_path / "sub" / "b.bin").write_bytes(b"y" * 500)
assert transfer.dir_size(str(tmp_path)) == 1500
def test_dir_size_missing_returns_zero():
assert transfer.dir_size("/nonexistent/path/xyz") == 0
# ---------- 补传重试 + 校验 ----------
def test_parse_args_retry():
args = transfer.parse_args(["--retry", "5", "s", "d"])
assert args.retry == 5
def test_parse_args_retry_default_zero():
assert transfer.parse_args(["s", "d"]).retry == 0
def test_parse_args_verify_flag():
args = transfer.parse_args(["--verify", "s", "d"])
assert args.verify is True
def test_rsync_verify_cmd():
cmd = transfer.RsyncBackend.build_verify_cmd("a/", "b/", bwlimit="30000")
joined = " ".join(cmd)
assert cmd[0] == "rsync"
assert "--checksum" in joined or "-c" in joined
assert "--dry-run" in joined
assert "--bwlimit=30000" in joined
def test_rsync_verify_cmd_no_bwlimit():
cmd = transfer.RsyncBackend.build_verify_cmd("a/", "b/")
assert "--bwlimit" not in " ".join(cmd)
# ---------- 暂停/继续/停止 (TUI 命令) ----------
def _proc_state(pid: int) -> str:
with open(f"/proc/{pid}/stat") as fh:
return fh.read().split()[2] # 状态字段: R/S/T/D/Z
def _make_runner(tmp_path) -> "transfer.TaskRunner":
cfg = transfer.TransferConfig("s", "d")
lm = transfer.LogManager(str(tmp_path), keep=20)
return transfer.TaskRunner(cfg, lm)
def test_pause_sends_sigstop(tmp_path):
r = _make_runner(tmp_path)
p = subprocess.Popen(["sleep", "30"], start_new_session=True)
r.proc = p
r.pause()
assert r._paused is True
# SIGSTOP 异步生效, 等待状态变 T
for _ in range(20):
if _proc_state(p.pid) == "T":
break
time.sleep(0.05)
assert _proc_state(p.pid) == "T" # T = stopped
p.kill()
p.wait()
def test_resume_sends_sigcont(tmp_path):
r = _make_runner(tmp_path)
p = subprocess.Popen(["sleep", "30"], start_new_session=True)
r.proc = p
r.pause()
r.resume()
assert r._paused is False
assert _proc_state(p.pid) in ("S", "R") # 已恢复
p.kill()
p.wait()
def test_pause_idempotent_when_no_proc(tmp_path):
r = _make_runner(tmp_path)
r.pause() # 无进程不报错
assert r._paused is False
# ---------- 文件计数 (单文件进度条数据源) ----------
def test_count_files_recursive(tmp_path):
(tmp_path / "a.flac").write_text("x")
(tmp_path / "sub").mkdir()
(tmp_path / "sub" / "b.lrc").write_text("y")
(tmp_path / "sub" / "c.flac").write_text("z")
assert transfer.count_files(str(tmp_path)) == 3
def test_count_files_missing_returns_zero():
assert transfer.count_files("/nonexistent/xyz") == 0
def test_count_files_only_files(tmp_path):
(tmp_path / "a.txt").write_text("x")
(tmp_path / "dir1").mkdir()
assert transfer.count_files(str(tmp_path)) == 1 # 目录不计
def test_parse_args_retry_infinite():
args = transfer.parse_args(["--retry", "-1", "s", "d"])
assert args.retry == -1 # 无限重试
# ---------- 智能断点续传 (校验最新N个文件) ----------
def test_verify_recent_all_ok(tmp_path):
src = tmp_path / "s"; dst = tmp_path / "d"
src.mkdir(); dst.mkdir()
(src / "a.bin").write_bytes(b"x" * 1000)
(dst / "a.bin").write_bytes(b"x" * 1000)
assert transfer.verify_recent_files(str(src), str(dst)) == []
def test_verify_recent_detects_corrupt_and_deletes(tmp_path):
src = tmp_path / "s"; dst = tmp_path / "d"
src.mkdir(); dst.mkdir()
(src / "a.bin").write_bytes(b"hello world")
(dst / "a.bin").write_bytes(b"hello worlX") # 内容损坏
bad = transfer.verify_recent_files(str(src), str(dst))
assert bad == ["a.bin"]
assert not (dst / "a.bin").exists() # 坏副本已删除
def test_verify_recent_empty_dst_no_check(tmp_path):
"""目标无已传文件时无可校验 (缺失文件由 rsync 正常补传)"""
src = tmp_path / "s"; dst = tmp_path / "d"
src.mkdir(); dst.mkdir()
(src / "a.bin").write_bytes(b"data")
assert transfer.verify_recent_files(str(src), str(dst)) == []
def test_verify_recent_large_file_checks_size_only(tmp_path):
""">100MB 只查大小, 不做内容 hash"""
src = tmp_path / "s"; dst = tmp_path / "d"
src.mkdir(); dst.mkdir()
big = b"a" * (101 * 1024 * 1024) # 101MB
(src / "big.bin").write_bytes(big)
(dst / "big.bin").write_bytes(b"b" * len(big)) # 同大小不同内容
assert transfer.verify_recent_files(str(src), str(dst)) == [] # 只查大小, 不报
def test_verify_recent_count_limit(tmp_path):
"""只校验最新 N 个, 更早的损坏不报"""
src = tmp_path / "s"; dst = tmp_path / "d"
src.mkdir(); dst.mkdir()
for i in range(3):
(src / f"f{i}.txt").write_text("ok" * 100)
(dst / f"f{i}.txt").write_text("ok" * 100)
import time
# 让 f0 是最早的
old = time.time() - 100
os.utime(str(dst / "f0.txt"), (old, old))
os.utime(str(src / "f0.txt"), (old, old))
# 损坏最早的文件 (不在最新2个内)
(src / "f0.txt").write_text("changed")
(dst / "f0.txt").write_text("changed-too")
bad = transfer.verify_recent_files(str(src), str(dst), check_count=2)
assert "f0.txt" not in bad # 只查最新2个
def test_parse_smart_resume():
assert transfer.parse_args(["s", "d"]).smart_resume is False
assert transfer.parse_args(["--smart-resume", "s", "d"]).smart_resume is True
assert transfer.parse_args(["--smart-check", "3", "s", "d"]).smart_check == 3
assert transfer.parse_args(["s", "d"]).smart_check == 5
# ---------- 校验进度回调 ----------
def test_verify_recent_progress_callback(tmp_path):
"""校验每个文件触发回调 (index, total, rel, ok)"""
src = tmp_path / "s"; dst = tmp_path / "d"
src.mkdir(); dst.mkdir()
for i in range(3):
(src / f"f{i}.txt").write_text("good" * 100)
(dst / f"f{i}.txt").write_text("good" * 100)
(dst / "f2.txt").write_text("corrupt") # 损坏一个
events = []
transfer.verify_recent_files(str(src), str(dst), check_count=3,
on_file=lambda i, n, r, ok: events.append((i, n, r, ok)))
assert len(events) == 3 # 每个文件都回调
assert all(n == 3 for _, n, _, _ in events) # total 一致
bad_events = [e for e in events if not e[3]]
assert len(bad_events) == 1 and bad_events[0][2] == "f2.txt" # 损坏的被标记
assert sum(1 for e in events if e[3]) == 2 # 其余通过
def test_verify_recent_callback_without_progress(tmp_path):
"""不带回调时行为不变 (向后兼容)"""
src = tmp_path / "s"; dst = tmp_path / "d"
src.mkdir(); dst.mkdir()
(src / "a.txt").write_text("x")
(dst / "a.txt").write_text("x")
assert transfer.verify_recent_files(str(src), str(dst)) == []
# ---------- 校验进度 (读取量轮询) ----------
def test_read_proc_io_current_process():
"""读取 /proc/self/io 的 read_bytes"""
val = transfer.read_proc_io(os.getpid())
assert isinstance(val, int)
assert val >= 0
def test_read_proc_io_nonexistent_pid():
assert transfer.read_proc_io(99999999) == 0
def test_collect_process_tree_self():
"""自身进程无子进程时 = 自身 read_bytes"""
me = transfer.collect_process_tree_read_bytes(os.getpid())
assert isinstance(me, int) and me >= 0
def test_collect_process_tree_with_child(tmp_path):
"""进程树统计不报错, 返回合理值 (缓存命中时 read_bytes 可能为 0)"""
f = tmp_path / "data.bin"
f.write_bytes(b"x" * (4 * 1024 * 1024))
child = subprocess.Popen(["cat", str(f)], stdout=subprocess.DEVNULL,
start_new_session=True)
time.sleep(0.5)
val = transfer.collect_process_tree_read_bytes(child.pid)
child.kill(); child.wait()
assert isinstance(val, int) and val >= 0
# ---------- 校验等级 + 纯校验模式 ----------
def test_parse_check_only():
args = transfer.parse_args(["--check-only", "s", "d"])
assert args.check_only is True
def test_parse_verify_level_default():
assert transfer.parse_args(["s", "d"]).verify_level == "slow"
assert transfer.parse_args(["--check-only", "s", "d"]).verify_level == "slow"
def test_parse_verify_level_values():
for lv in ("ultrafast", "fast", "normal", "slow", "full"):
args = transfer.parse_args(["--verify-level", lv, "s", "d"])
assert args.verify_level == lv
def test_size_check_all_ok(tmp_path):
src = tmp_path / "s"; dst = tmp_path / "d"
src.mkdir(); dst.mkdir()
(src / "a.bin").write_bytes(b"x" * 1000)
(dst / "a.bin").write_bytes(b"x" * 1000)
assert transfer.size_check(str(src), str(dst)) == ([], [])
def test_size_check_detect_diff_and_missing(tmp_path):
src = tmp_path / "s"; dst = tmp_path / "d"
src.mkdir(); dst.mkdir()
(src / "a.bin").write_bytes(b"x" * 1000)
(dst / "a.bin").write_bytes(b"x" * 999) # 大小不同
(src / "b.bin").write_bytes(b"y" * 100)
# b.bin 目标缺失
diffs, missing = transfer.size_check(str(src), str(dst))
assert diffs == ["a.bin"] and missing == ["b.bin"]
def test_verify_level_order():
"""等级从快到慢的顺序固定"""
assert transfer.VERIFY_LEVELS == ["ultrafast", "fast", "normal", "slow", "full"]
# ---------- 各等级校验进度 ----------
def test_stat_check_detects_size_and_mtime(tmp_path):
src = tmp_path / "s"; dst = tmp_path / "d"
src.mkdir(); dst.mkdir()
(src / "ok.txt").write_text("good")
(dst / "ok.txt").write_text("good")
(src / "diff.txt").write_text("longer content here")
(dst / "diff.txt").write_text("short")
(src / "gone.txt").write_text("x")
# gone.txt 目标缺失
diffs, missing = transfer.stat_check(str(src), str(dst))
assert "diff.txt" in diffs # 大小不同
assert "gone.txt" in missing # 缺失
assert "ok.txt" not in diffs
def test_stat_check_mtime_diff(tmp_path):
"""mtime 不同但大小相同 → 记为差异"""
src = tmp_path / "s"; dst = tmp_path / "d"
src.mkdir(); dst.mkdir()
(src / "a.txt").write_text("same")
(dst / "a.txt").write_text("same")
import time as _t
old = _t.time() - 500
os.utime(str(dst / "a.txt"), (old, old))
diffs, _missing = transfer.stat_check(str(src), str(dst))
assert "a.txt" in diffs
def test_stat_check_progress_callback(tmp_path):
src = tmp_path / "s"; dst = tmp_path / "d"
src.mkdir(); dst.mkdir()
for i in range(50):
(src / f"f{i}.txt").write_text("x")
(dst / f"f{i}.txt").write_text("x")
events = []
transfer.stat_check(str(src), str(dst),
on_progress=lambda i, n: events.append((i, n)))
assert len(events) > 0 # 有进度回调
assert events[-1] == (50, 50) # 最终回调
def test_size_check_progress_callback(tmp_path):
src = tmp_path / "s"; dst = tmp_path / "d"
src.mkdir(); dst.mkdir()
for i in range(30):
(src / f"f{i}.txt").write_text("x")
(dst / f"f{i}.txt").write_text("x")
events = []
transfer.size_check(str(src), str(dst),
on_progress=lambda i, n: events.append((i, n)))
assert events and events[-1][0] == events[-1][1]
# ---------- 校验后修复 (--repair) ----------
def test_parse_repair():
assert transfer.parse_args(["s", "d"]).repair is False
assert transfer.parse_args(["--repair", "s", "d"]).repair is True
def test_build_repair_cmd(tmp_path):
list_file = str(tmp_path / "list.txt")
with open(list_file, "w") as f:
f.write("a.flac\nsub/b.lrc\n")
cmd, used = transfer.build_repair_cmd("src/", "dst/", list_file, "30000")
joined = " ".join(cmd)
assert cmd[0] == "rsync"
assert f"--files-from={list_file}" in joined
assert "--mkpath" in joined
assert "--bwlimit=30000" in joined
assert "src/" in joined and "dst/" in joined
# ---------- 重传后复核 ----------
def test_verify_files_list_all_ok(tmp_path):
src = tmp_path / "s"; dst = tmp_path / "d"
src.mkdir(); dst.mkdir()
for f in ("a.flac", "b.lrc"):
(src / f).write_bytes(b"data" * 100)
(dst / f).write_bytes(b"data" * 100)
ok, fail = transfer.verify_files_list(str(src), str(dst), ["a.flac", "b.lrc"])
assert set(ok) == {"a.flac", "b.lrc"} and fail == []
def test_verify_files_list_detect_fail(tmp_path):
src = tmp_path / "s"; dst = tmp_path / "d"
src.mkdir(); dst.mkdir()
(src / "a.flac").write_bytes(b"good content")
(dst / "a.flac").write_bytes(b"bad content!!")
ok, fail = transfer.verify_files_list(str(src), str(dst), ["a.flac"])
assert fail == ["a.flac"] and ok == []
def test_verify_files_list_ultrafast_size_only(tmp_path):
"""> 同大小不同内容: ultrafast 判通过, slow 判失败"""
src = tmp_path / "s"; dst = tmp_path / "d"
src.mkdir(); dst.mkdir()
data = b"x" * 1000
(src / "a.flac").write_bytes(data)
(dst / "a.flac").write_bytes(b"y" * 1000)
ok, _ = transfer.verify_files_list(str(src), str(dst), ["a.flac"], level="ultrafast")
assert ok == ["a.flac"] # 只查大小
ok2, fail2 = transfer.verify_files_list(str(src), str(dst), ["a.flac"], level="slow")
assert fail2 == ["a.flac"] # md5 查出
def test_verify_files_list_missing_target(tmp_path):
src = tmp_path / "s"; dst = tmp_path / "d"
src.mkdir(); dst.mkdir()
(src / "a.flac").write_bytes(b"x")
ok, fail = transfer.verify_files_list(str(src), str(dst), ["a.flac"])
assert fail == ["a.flac"]