Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

373 lines
12 KiB
Python

"""
Tests for globals.py — thread-safe accessor functions.
Tests cover:
- Accessor functions for active_research, termination_flags
- Thread safety (concurrent read/write)
- Atomic operations (update_progress_if_higher, cleanup_research)
- Snapshot isolation (returned data cannot mutate shared state)
"""
import threading
import pytest
from local_deep_research.web.routes import globals as g
@pytest.fixture(autouse=True)
def _clean_globals():
"""Reset all global state before each test."""
with g._lock:
g._active_research.clear()
g._termination_flags.clear()
yield
with g._lock:
g._active_research.clear()
g._termination_flags.clear()
# ===================================================================
# active_research accessors
# ===================================================================
class TestIsResearchActive:
def test_returns_false_when_empty(self):
assert g.is_research_active("r1") is False
def test_returns_true_when_present(self):
g.set_active_research("r1", {"progress": 0})
assert g.is_research_active("r1") is True
def test_returns_false_after_removal(self):
g.set_active_research("r1", {"progress": 0})
g.remove_active_research("r1")
assert g.is_research_active("r1") is False
class TestGetActiveResearchIds:
def test_empty(self):
assert g.get_active_research_ids() == []
def test_returns_all_ids(self):
g.set_active_research("r1", {})
g.set_active_research("r2", {})
ids = g.get_active_research_ids()
assert sorted(ids) == ["r1", "r2"]
def test_returns_snapshot(self):
"""Returned list should not be affected by later mutations."""
g.set_active_research("r1", {})
ids = g.get_active_research_ids()
g.set_active_research("r2", {})
assert ids == ["r1"]
class TestGetActiveResearchSnapshot:
def test_returns_none_when_missing(self):
assert g.get_active_research_snapshot("r1") is None
def test_returns_projected_fields(self):
g.set_active_research(
"r1",
{
"thread": threading.current_thread(),
"progress": 42,
"status": "in_progress",
"log": [{"time": "t1", "message": "msg"}],
"settings": {"key": "val"},
},
)
snap = g.get_active_research_snapshot("r1")
assert snap["progress"] == 42
assert snap["status"] == "in_progress"
assert len(snap["log"]) == 1
assert snap["settings"] == {"key": "val"}
assert "thread" not in snap
def test_log_is_a_copy(self):
"""Mutating the returned log should not affect shared state."""
g.set_active_research(
"r1",
{
"log": [{"m": "a"}],
"progress": 0,
"status": None,
"settings": None,
},
)
snap = g.get_active_research_snapshot("r1")
snap["log"].append({"m": "b"})
assert len(g.get_active_research_snapshot("r1")["log"]) == 1
def test_settings_is_a_copy(self):
"""Mutating returned settings must not affect internal state."""
g.set_active_research(
"r1", {"settings": {"key": "val"}, "log": [], "progress": 0}
)
snap = g.get_active_research_snapshot("r1")
snap["settings"]["injected"] = True
assert (
"injected" not in g.get_active_research_snapshot("r1")["settings"]
)
class TestGetResearchField:
def test_returns_default_when_missing(self):
assert g.get_research_field("r1", "progress", 99) == 99
def test_returns_value(self):
g.set_active_research("r1", {"progress": 50})
assert g.get_research_field("r1", "progress") == 50
def test_returns_shallow_copy_of_list(self):
g.set_active_research("r1", {"log": [1, 2, 3]})
log = g.get_research_field("r1", "log")
log.append(4)
assert g.get_research_field("r1", "log") == [1, 2, 3]
def test_returns_shallow_copy_of_dict(self):
g.set_active_research("r1", {"settings": {"a": 1}})
s = g.get_research_field("r1", "settings")
s["b"] = 2
assert "b" not in g.get_research_field("r1", "settings")
class TestSetActiveResearch:
def test_insert(self):
g.set_active_research("r1", {"progress": 0})
assert g.is_research_active("r1")
def test_overwrite(self):
g.set_active_research("r1", {"progress": 10})
g.set_active_research("r1", {"progress": 20})
assert g.get_research_field("r1", "progress") == 20
class TestUpdateActiveResearch:
def test_updates_existing(self):
g.set_active_research("r1", {"progress": 0, "status": "pending"})
g.update_active_research("r1", progress=50)
assert g.get_research_field("r1", "progress") == 50
assert g.get_research_field("r1", "status") == "pending"
def test_noop_when_missing(self):
# Should not raise
g.update_active_research("r1", progress=50)
class TestAppendResearchLog:
def test_appends(self):
g.set_active_research("r1", {"log": []})
g.append_research_log("r1", {"time": "t1"})
g.append_research_log("r1", {"time": "t2"})
assert g.get_research_field("r1", "log") == [
{"time": "t1"},
{"time": "t2"},
]
def test_creates_log_list_if_missing(self):
g.set_active_research("r1", {})
g.append_research_log("r1", {"time": "t1"})
assert g.get_research_field("r1", "log") == [{"time": "t1"}]
def test_noop_when_research_missing(self):
g.append_research_log("r1", {"time": "t1"})
class TestUpdateProgressIfHigher:
def test_updates_when_higher(self):
g.set_active_research("r1", {"progress": 10})
result = g.update_progress_if_higher("r1", 50)
assert result == 50
assert g.get_research_field("r1", "progress") == 50
def test_does_not_go_backwards(self):
g.set_active_research("r1", {"progress": 80})
result = g.update_progress_if_higher("r1", 50)
assert result == 80
assert g.get_research_field("r1", "progress") == 80
def test_returns_none_when_missing(self):
assert g.update_progress_if_higher("r1", 50) is None
def test_handles_none_progress(self):
g.set_active_research("r1", {"progress": 30})
result = g.update_progress_if_higher("r1", None)
assert result == 30
class TestRemoveActiveResearch:
def test_removes(self):
g.set_active_research("r1", {})
g.remove_active_research("r1")
assert not g.is_research_active("r1")
def test_noop_when_missing(self):
g.remove_active_research("r1") # Should not raise
class TestIterActiveResearch:
def test_yields_all_entries(self):
g.set_active_research("r1", {"progress": 10})
g.set_active_research("r2", {"progress": 20})
entries = dict(g.iter_active_research())
assert "r1" in entries
assert "r2" in entries
assert entries["r1"]["progress"] == 10
def test_yields_copies(self):
g.set_active_research("r1", {"progress": 10})
for _, data in g.iter_active_research():
data["progress"] = 999
assert g.get_research_field("r1", "progress") == 10
def test_does_not_hold_lock_during_iteration(self):
"""Another thread should be able to write while we iterate."""
g.set_active_research("r1", {"progress": 10})
g.set_active_research("r2", {"progress": 20})
write_succeeded = threading.Event()
for _, _ in g.iter_active_research():
# Mid-iteration: a writer thread should not be blocked
def writer():
g.set_active_research("r3", {"progress": 30})
write_succeeded.set()
t = threading.Thread(target=writer)
t.start()
t.join(timeout=2)
break # Only need one iteration to test
assert write_succeeded.is_set(), "Writer was blocked by iter lock"
class TestGetActiveResearchCount:
def test_empty(self):
assert g.get_active_research_count() == 0
def test_counts(self):
g.set_active_research("r1", {})
g.set_active_research("r2", {})
assert g.get_active_research_count() == 2
# ===================================================================
# termination_flags accessors
# ===================================================================
class TestTerminationFlags:
def test_default_is_false(self):
assert g.is_termination_requested("r1") is False
def test_set_and_check(self):
g.set_termination_flag("r1")
assert g.is_termination_requested("r1") is True
def test_clear(self):
g.set_termination_flag("r1")
g.clear_termination_flag("r1")
assert g.is_termination_requested("r1") is False
def test_clear_noop_when_missing(self):
g.clear_termination_flag("r1") # Should not raise
def test_independent_flags(self):
g.set_termination_flag("r1")
assert g.is_termination_requested("r1") is True
assert g.is_termination_requested("r2") is False
# ===================================================================
# cleanup_research
# ===================================================================
class TestCleanupResearch:
def test_removes_from_all_dicts(self):
g.set_active_research("r1", {"progress": 50})
g.set_termination_flag("r1")
g.cleanup_research("r1")
assert not g.is_research_active("r1")
assert not g.is_termination_requested("r1")
def test_noop_when_missing(self):
g.cleanup_research("r1") # Should not raise
# ===================================================================
# Thread safety
# ===================================================================
class TestThreadSafety:
def test_concurrent_progress_updates(self):
"""Multiple threads updating progress should not lose data."""
g.set_active_research("r1", {"progress": 0})
barrier = threading.Barrier(10)
def updater(value):
barrier.wait()
g.update_progress_if_higher("r1", value)
threads = [
threading.Thread(target=updater, args=(i * 10,)) for i in range(10)
]
for t in threads:
t.start()
for t in threads:
t.join()
# Progress should be the maximum value
assert g.get_research_field("r1", "progress") == 90
def test_concurrent_set_and_remove(self):
"""Concurrent add/remove should not crash."""
errors = []
def worker(i):
try:
for _ in range(100):
rid = f"r-{i}"
g.set_active_research(rid, {"progress": 0})
g.is_research_active(rid)
g.remove_active_research(rid)
except Exception as e:
errors.append(e)
threads = [threading.Thread(target=worker, args=(i,)) for i in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
assert errors == []
def test_concurrent_cleanup(self):
"""cleanup_research from multiple threads should not crash."""
for i in range(20):
g.set_active_research(f"r-{i}", {"progress": i})
g.set_termination_flag(f"r-{i}")
errors = []
def cleaner(i):
try:
g.cleanup_research(f"r-{i}")
except Exception as e:
errors.append(e)
threads = [
threading.Thread(target=cleaner, args=(i,)) for i in range(20)
]
for t in threads:
t.start()
for t in threads:
t.join()
assert errors == []
assert g.get_active_research_count() == 0