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.
144 lines
4.9 KiB
Python
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()
|