Files
ComfyAssets-ComfyUI_PromptM…/tests/test_preview_probe.py
T
Vito 2e9322d5c4 fix(thumbnails): never let an older folder check overwrite a newer answer (#611) (#612)
Two refresh() callers could publish out of order. A check that looked
before the folder was removed could set "available" after a newer check
said "missing", and the state stayed wrong until the next probe. Each
check now has an order number, and an older answer is dropped.
2026-11-01 21:16:55 +00:00

144 lines
4.9 KiB
Python

"""The thumbnail folder check answers for the folder as it is now (#320).
A check that started before the caller asked can miss what just happened: #592
hit it in the merge queue, where a folder deleted right after
``configure`` woke the probe stayed "available" until the next probe.
"""
import sys
import threading
import unittest
from pathlib import Path
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from py.api import preview_location, preview_store # noqa: E402
WAIT = 5
HERE = (True, None, {"id": "x"})
GONE = (False, "missing", None)
KEY = ("/thumbs", "x")
class TestProbeCheck(unittest.TestCase):
def setUp(self):
self.probe = preview_store._Probe()
def test_a_finished_check_nobody_collected_is_not_the_answer(self):
with patch.object(preview_location, "folder_state", return_value=HERE):
with self.probe._lock:
_key, done, _result = self.probe._start_check(*KEY)
self.assertTrue(done.wait(WAIT))
with patch.object(preview_location, "folder_state", return_value=GONE):
self.assertEqual(self.probe.check(*KEY), GONE)
def test_a_check_already_running_is_followed_by_a_new_one(self):
started = threading.Event()
release = threading.Event()
calls = []
def state(_path, _expected_id):
calls.append(threading.current_thread().name)
if len(calls) == 1:
started.set()
self.assertTrue(release.wait(WAIT))
return HERE
return GONE
answers = []
with patch.object(preview_location, "folder_state", state):
with self.probe._lock:
self.probe._start_check(*KEY)
self.assertTrue(started.wait(WAIT))
caller = threading.Thread(
target=lambda: answers.append(self.probe.check(*KEY))
)
caller.start()
release.set()
caller.join(WAIT)
self.assertFalse(caller.is_alive())
self.assertEqual(answers, [GONE])
self.assertEqual(len(calls), 2) # one after the other, never two at once
def test_a_stuck_check_is_never_doubled(self):
release = threading.Event()
self.addCleanup(release.set)
calls = []
def state(_path, _expected_id):
calls.append(1)
release.wait(WAIT)
return HERE
with (
patch.object(preview_location, "folder_state", state),
patch.object(preview_store, "IO_BUDGET", 0.05),
):
with self.probe._lock:
self.probe._start_check(*KEY)
with self.assertRaises(TimeoutError):
self.probe.check(*KEY)
self.assertEqual(len(calls), 1)
def test_an_error_from_the_new_check_reaches_the_caller(self):
with patch.object(
preview_location, "folder_state", side_effect=OSError("share gone")
):
with self.assertRaises(OSError):
self.probe.check(*KEY)
class TestRefreshOrder(unittest.TestCase):
"""Only the newest check's answer is published (#611)."""
def test_an_older_check_never_overwrites_a_newer_answer(self):
parsed = preview_location.ParsedLocation(
mode=preview_store.FOLDER_MODE, path="/thumbs", id="x"
)
self.addCleanup(setattr, preview_store, "_available", preview_store._available)
self.addCleanup(setattr, preview_store, "_problem", preview_store._problem)
started = threading.Event()
release = threading.Event()
newer_published = threading.Event()
calls = []
publish = preview_store._set_state
def state(_path, _expected_id):
calls.append(1)
if len(calls) == 1:
started.set()
self.assertTrue(release.wait(WAIT))
return HERE
return GONE
def set_state(*args, **kwargs):
if args[1]: # the older check's "available" arrives last
self.assertTrue(newer_published.wait(WAIT))
publish(*args, **kwargs)
def newer():
preview_store.refresh()
newer_published.set()
with (
patch.object(preview_store, "_parsed", parsed),
patch.object(preview_location, "folder_state", state),
patch.object(preview_store, "_set_state", set_state),
):
first = threading.Thread(target=preview_store.refresh)
first.start()
self.assertTrue(started.wait(WAIT))
second = threading.Thread(target=newer)
second.start()
release.set()
second.join(WAIT)
first.join(WAIT)
self.assertFalse(first.is_alive() or second.is_alive())
self.assertFalse(preview_store._available)
self.assertEqual(preview_store._problem, "missing")
if __name__ == "__main__":
unittest.main()