Files
sentinel-home-ai/fam-edge/tests/test_video_queue.py
ericwyuan deaebf22b4 fix(fam-edge): 生产者扫描跳过已被 DiskGuard 清理的文件
生产者列目录之后、读 mtime 之前,DiskGuard 可能刚好把那个文件删了(两个后台线程
的正常竞态),os.path.getmtime 抛 FileNotFoundError。代价不是少处理一个文件,而是
**整轮扫描中断**——排在后面的新素材本轮全都登记不上,要等下一轮。

线上日志:生产者扫描异常: [Errno 2] No such file or directory:
/opt/fam-edge/gdrive_videos/20260911PM/Generic_ONVIF-001-20260911-234730-...mp4
(01:30:01 抛出,同一秒 DiskGuard 正在清理那批文件)

改成捕获 OSError 跳过该文件继续扫。新增 tests/test_video_queue.py:被删的跳过且
后面的照常登记、仍在写入的(mtime 太新)依然跳过。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 09:35:15 +08:00

74 lines
2.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""VideoQueue 生产者扫描的单测。
目前只覆盖一个点:生产者列目录之后、读 mtime 之前DiskGuard 可能刚好把文件清掉
(两个后台线程的正常竞态)。线上表现为 `生产者扫描异常: [Errno 2] No such file or
directory`,代价是**整轮扫描中断**——排在后面的新素材本轮全都登记不上,要等下一轮。
"""
import os
import pytest
from fam_edge import video_queue as vq_mod
from fam_edge.video_queue import VideoQueue
class _FakeDB:
"""只实现生产者路径上用到的方法。"""
def __init__(self):
self.registered = []
self.activities = []
def get_video_by_filename(self, fn):
return None
def ensure_video(self, fn, path, camera_name=None):
self.registered.append(fn)
return len(self.registered)
def set_video_file_status(self, vid, valid, err, meta=None):
pass
def record_activity(self, service, action, detail=''):
self.activities.append((service, action))
@pytest.fixture
def queue_with_two_files(tmp_path, monkeypatch):
monkeypatch.setattr(vq_mod, 'load_config', lambda: {
'gdrive_sync': {'local_dir': str(tmp_path), 'camera_name': '客厅'},
'video_processing': {'file_validate': True, 'stable_window_sec': 60},
})
(tmp_path / "a_被删掉的.mp4").write_bytes(b"x")
(tmp_path / "b_正常的.mp4").write_bytes(b"y")
db = _FakeDB()
q = VideoQueue(db)
monkeypatch.setattr(q, '_enqueue', lambda vid: None)
monkeypatch.setattr(vq_mod, 'validate_video', lambda p: (True, '', {'fps': 25}))
return q, db
def test_produce_skips_file_deleted_mid_scan_and_keeps_going(queue_with_two_files, monkeypatch):
"""被删掉的那个跳过,后面的照常登记——不能整轮中断。"""
q, db = queue_with_two_files
real_getmtime = os.path.getmtime
def fake_getmtime(path):
if 'a_被删掉的' in path:
raise FileNotFoundError(2, 'No such file or directory', path)
return real_getmtime(path) - 3600 # 早于稳定窗口,视为写入完成
monkeypatch.setattr(vq_mod.os.path, 'getmtime', fake_getmtime)
q._produce_once() # 不抛异常
assert db.registered == ["b_正常的.mp4"] # 被删的跳过,后面的没受影响
def test_produce_still_skips_files_being_written(queue_with_two_files, monkeypatch):
"""mtime 太新rclone 还在写)仍然要跳过,别把半成品入队。"""
q, db = queue_with_two_files
monkeypatch.setattr(vq_mod.os.path, 'getmtime', lambda p: __import__('time').time())
q._produce_once()
assert db.registered == []